mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-21 07:28:26 +03:00
Revert "Feature/multiple jwt secrets"
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3136 Co-authored-by: pranshi06 <85474619+pranshi06@users.noreply.github.com> GitOrigin-RevId: aa41817e39f932f909067f2effca9d9973a5fb94
This commit is contained in:
parent
5726852c54
commit
4121c1dd3d
@ -31,4 +31,3 @@ Features
|
||||
api-limits
|
||||
disable-graphql-introspection
|
||||
rotating-admin-secrets
|
||||
multiple-jwt-secrets
|
||||
|
@ -1,43 +0,0 @@
|
||||
.. 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.
|
||||
|
||||
A single JWT secret can be provided without an `issuer`. Providing multiple secrets without an `issuer` will result in a configuration error at startup.
|
||||
|
||||
Bearer Tokens are authenticated against the secret with a matching `issuer`.
|
||||
|
||||
The authentication is resolved as follows:
|
||||
|
||||
1. The `issuer` is decoded from the bearer token.
|
||||
2. A JWT secret is looked up by `issuer` in the JWT secrets array.
|
||||
3. If a secret with a matching `issuer` is found then authenticate the token against that secret.
|
||||
4. If no secret is found or if the bearer token contains no `issuer` then verify against
|
||||
the `no-issuer` secret.
|
||||
5. If there is not a `no-issuer` secret then return an auth failure.
|
||||
|
||||
.. note::
|
||||
Authentication resolution is unchanged when using a single JWT secret.
|
||||
|
||||
.. note::
|
||||
|
||||
If both ``HASURA_GRAPHQL_JWT_SECRET`` and ``HASURA_GRAPHQL_JWT_SECRETS`` are set, then only ``HASURA_GRAPHQL_JWT_SECRETS`` will be used.
|
@ -156,7 +156,6 @@ library
|
||||
, mtl
|
||||
, openapi3
|
||||
, optparse-applicative
|
||||
, optparse-generic
|
||||
, parsec
|
||||
, pg-client
|
||||
, postgresql-binary
|
||||
|
@ -21,7 +21,6 @@ module Hasura.Prelude
|
||||
liftEitherM,
|
||||
hoistMaybe,
|
||||
hoistEither,
|
||||
readJson,
|
||||
tshow,
|
||||
|
||||
-- * Trace debugging
|
||||
@ -268,9 +267,6 @@ 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.
|
||||
--
|
||||
|
@ -7,7 +7,6 @@ module Hasura.Server.API.Config
|
||||
where
|
||||
|
||||
import Data.Aeson.TH
|
||||
import Data.HashMap.Strict qualified as Map
|
||||
import Data.HashSet qualified as Set
|
||||
import Hasura.GraphQL.Execute.LiveQuery.Options qualified as LQ
|
||||
import Hasura.Prelude
|
||||
@ -36,7 +35,7 @@ data ServerConfig = ServerConfig
|
||||
scfgIsAdminSecretSet :: !Bool,
|
||||
scfgIsAuthHookSet :: !Bool,
|
||||
scfgIsJwtSet :: !Bool,
|
||||
scfgJwt :: !(Map.HashMap (Maybe StringOrURI) JWTInfo),
|
||||
scfgJwt :: !(Maybe JWTInfo),
|
||||
scfgIsAllowListEnabled :: !Bool,
|
||||
scfgLiveQueries :: !LQ.LiveQueriesOptions,
|
||||
scfgConsoleAssetsDir :: !(Maybe Text),
|
||||
@ -91,12 +90,11 @@ isJWTSet = \case
|
||||
AMAdminSecretAndJWT {} -> True
|
||||
_ -> False
|
||||
|
||||
getJWTInfo :: AuthMode -> Map.HashMap (Maybe StringOrURI) 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
|
||||
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
|
||||
|
@ -33,7 +33,6 @@ import Control.Monad.Trans.Managed (ManagedT)
|
||||
import Crypto.Hash qualified as Crypto
|
||||
import Data.ByteArray qualified as BA
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.HashMap.Strict qualified as Map
|
||||
import Data.HashSet qualified as Set
|
||||
import Data.Hashable qualified as Hash
|
||||
import Data.IORef (newIORef)
|
||||
@ -97,7 +96,7 @@ data AuthMode
|
||||
= AMNoAuth
|
||||
| AMAdminSecret !(Set.HashSet AdminSecretHash) !(Maybe RoleName)
|
||||
| AMAdminSecretAndHook !(Set.HashSet AdminSecretHash) !AuthHook
|
||||
| AMAdminSecretAndJWT !(Set.HashSet AdminSecretHash) !(Map.HashMap (Maybe StringOrURI) JWTCtx) !(Maybe RoleName)
|
||||
| AMAdminSecretAndJWT !(Set.HashSet AdminSecretHash) !JWTCtx !(Maybe RoleName)
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Validate the user's requested authentication configuration, launching any
|
||||
@ -110,17 +109,17 @@ setupAuthMode ::
|
||||
) =>
|
||||
Set.HashSet AdminSecretHash ->
|
||||
Maybe AuthHook ->
|
||||
Map.HashMap (Maybe StringOrURI) JWTConfig ->
|
||||
Maybe JWTConfig ->
|
||||
Maybe RoleName ->
|
||||
H.Manager ->
|
||||
Logger Hasura ->
|
||||
ExceptT Text (ManagedT m) AuthMode
|
||||
setupAuthMode adminSecretHashSet mWebHook mJwtSecrets mUnAuthRole httpManager logger =
|
||||
case (not (Set.null adminSecretHashSet), mWebHook, not (Map.null mJwtSecrets)) of
|
||||
(True, Nothing, False) -> return $ AMAdminSecret adminSecretHashSet mUnAuthRole
|
||||
(True, Nothing, True) -> do
|
||||
jwtCtxs <- mkJwtCtxs mJwtSecrets
|
||||
pure $ AMAdminSecretAndJWT adminSecretHashSet jwtCtxs mUnAuthRole
|
||||
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
|
||||
-- Nothing below this case uses unauth role. Throw a fatal error if we would otherwise ignore
|
||||
-- that parameter, lest users misunderstand their auth configuration:
|
||||
_
|
||||
@ -129,26 +128,18 @@ setupAuthMode adminSecretHashSet mWebHook mJwtSecrets mUnAuthRole httpManager lo
|
||||
"Fatal Error: --unauthorized-role (HASURA_GRAPHQL_UNAUTHORIZED_ROLE)"
|
||||
<> requiresAdminScrtMsg
|
||||
<> " and is not allowed when --auth-hook (HASURA_GRAPHQL_AUTH_HOOK) is set"
|
||||
(False, Nothing, False) -> return AMNoAuth
|
||||
(True, Just hook, False) -> return $ AMAdminSecretAndHook adminSecretHashSet hook
|
||||
(False, Just _, False) ->
|
||||
(False, Nothing, Nothing) -> return AMNoAuth
|
||||
(True, Just hook, Nothing) -> return $ AMAdminSecretAndHook adminSecretHashSet hook
|
||||
(False, Just _, Nothing) ->
|
||||
throwError $
|
||||
"Fatal Error : --auth-hook (HASURA_GRAPHQL_AUTH_HOOK)" <> requiresAdminScrtMsg
|
||||
(False, Nothing, True) ->
|
||||
(False, Nothing, Just _) ->
|
||||
throwError $
|
||||
"Fatal Error : --jwt-secret (HASURA_GRAPHQL_JWT_SECRET)" <> requiresAdminScrtMsg
|
||||
(_, Just _, True) ->
|
||||
(_, Just _, Just _) ->
|
||||
throwError
|
||||
"Fatal Error: Both webhook and JWT mode cannot be enabled at the same time"
|
||||
where
|
||||
mkJwtCtxs ::
|
||||
( ForkableMonadIO m,
|
||||
Tracing.HasReporter m
|
||||
) =>
|
||||
Map.HashMap (Maybe StringOrURI) JWTConfig ->
|
||||
ExceptT Text (ManagedT m) (Map.HashMap (Maybe StringOrURI) JWTCtx)
|
||||
mkJwtCtxs = traverse mkJwtCtx
|
||||
|
||||
requiresAdminScrtMsg =
|
||||
" requires --admin-secret (HASURA_GRAPHQL_ADMIN_SECRET) or "
|
||||
<> " --access-key (HASURA_GRAPHQL_ACCESS_KEY) to be set"
|
||||
@ -213,7 +204,7 @@ getUserInfoWithExpTime_ ::
|
||||
m (UserInfo, Maybe UTCTime, [N.Header])
|
||||
) ->
|
||||
-- | mock 'processJwt'
|
||||
(Map.HashMap (Maybe StringOrURI) 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] ->
|
||||
@ -241,8 +232,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 jwtSecrets unAuthRole ->
|
||||
checkingSecretIfSent adminSecretHashSet $ processJwt_ jwtSecrets rawHeaders unAuthRole
|
||||
AMAdminSecretAndJWT adminSecretHashSet jwtSecret unAuthRole ->
|
||||
checkingSecretIfSent adminSecretHashSet $ processJwt_ jwtSecret rawHeaders unAuthRole
|
||||
where
|
||||
-- CAREFUL!:
|
||||
mkUserInfoFallbackAdminRole adminSecretState =
|
||||
|
@ -1,7 +1,6 @@
|
||||
module Hasura.Server.Auth.JWT
|
||||
( processJwt,
|
||||
RawJWT,
|
||||
StringOrURI (..),
|
||||
JWTConfig (..),
|
||||
JWTCtx (..),
|
||||
Jose.JWKSet (..),
|
||||
@ -38,13 +37,10 @@ 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.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
|
||||
@ -220,21 +216,6 @@ 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),
|
||||
@ -376,21 +357,6 @@ updateJwkRef (Logger logger) manager url jwkRef = do
|
||||
|
||||
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
|
||||
@ -407,7 +373,7 @@ processJwt ::
|
||||
( MonadIO m,
|
||||
MonadError QErr m
|
||||
) =>
|
||||
Map.HashMap (Maybe StringOrURI) JWTCtx ->
|
||||
JWTCtx ->
|
||||
HTTP.RequestHeaders ->
|
||||
Maybe RoleName ->
|
||||
m (UserInfo, Maybe UTCTime, [N.Header])
|
||||
@ -419,49 +385,29 @@ processJwt_ ::
|
||||
-- | mock 'processAuthZOrCookieHeader'
|
||||
(_JWTCtx -> BLC.ByteString -> m (Maybe (ClaimsMap, Maybe UTCTime))) ->
|
||||
(_JWTCtx -> JWTHeader) ->
|
||||
Map.HashMap (Maybe StringOrURI) _JWTCtx ->
|
||||
_JWTCtx ->
|
||||
HTTP.RequestHeaders ->
|
||||
Maybe RoleName ->
|
||||
m (UserInfo, Maybe UTCTime, [N.Header])
|
||||
processJwt_ processAuthZOrCookieHeader_ fGetHeaderType jwtCtxs headers mUnAuthRole =
|
||||
case find (liftA2 (||) (== CI.mk "Cookie") (== CI.mk "Authorization") . fst) headers of
|
||||
Nothing -> withoutAuthZHeader
|
||||
Just (key, val) ->
|
||||
let issuer = tokenIssuer (RawJWT $ BLC.fromStrict val)
|
||||
in case (Map.size jwtCtxs, Map.elems jwtCtxs, Map.lookup issuer jwtCtxs) of
|
||||
-- Note: In 2.1 prior to this commit, if the JWTCtx has
|
||||
-- an Issuer specified, then a token with a matching or
|
||||
-- absent issuer will validate, but a different issuer
|
||||
-- will fail. We special case that by checking if the
|
||||
-- size of the HashMap is 1:
|
||||
(1, [jwtCtx], Nothing) -> withAuthZHeader val jwtCtx
|
||||
(_, _, Nothing) ->
|
||||
case Map.lookup Nothing jwtCtxs of
|
||||
Just jwtCtx -> withAuthZHeader val jwtCtx
|
||||
Nothing -> throw400 InvalidHeaders "Could not verify JWT: Invalid Issuer"
|
||||
(_, _, Just jwtCtx)
|
||||
| key /= expectedHeader jwtCtx ->
|
||||
throw400 InvalidHeaders $ "Missing " <> T.decodeUtf8 (CI.foldedCase $ expectedHeader jwtCtx) <> " header in JWT authentication mode"
|
||||
(_, _, Just jwtCtx) -> withAuthZHeader val jwtCtx
|
||||
processJwt_ processAuthZOrCookieHeader_ fGetHeaderType jwtCtx headers mUnAuthRole =
|
||||
maybe withoutAuthZHeader withAuthZHeader mAuthZHeader
|
||||
where
|
||||
expectedHeader jwtCtx =
|
||||
expectedHeader =
|
||||
case fGetHeaderType jwtCtx of
|
||||
JHAuthorization -> CI.mk "Authorization"
|
||||
JHCookie _ -> CI.mk "Cookie"
|
||||
JHAuthorization -> "Authorization"
|
||||
JHCookie _ -> "Cookie"
|
||||
|
||||
withAuthZHeader authzHeader jwtCtx = do
|
||||
mAuthZHeader =
|
||||
find (\(headerName, _) -> headerName == CI.mk expectedHeader) headers
|
||||
|
||||
withAuthZHeader (_, authzHeader) = do
|
||||
claimsMapTuple <- processAuthZOrCookieHeader_ jwtCtx (BL.fromStrict authzHeader)
|
||||
-- if the requested cookie name is not present among the cookies available,
|
||||
-- then fall back to unauthorized-role (if present)
|
||||
|
||||
case claimsMapTuple of
|
||||
Nothing -> withoutAuthZHeader
|
||||
Just (claimsMap, expTimeM) -> 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
|
||||
let requestedRole = fromMaybe defaultRole $ getRequestHeader userRoleHeader headers >>= mkRoleName . bsToTxt
|
||||
|
||||
when (requestedRole `notElem` allowedRoles) $
|
||||
throw400 AccessDenied "Your requested role is not in allowed roles"
|
||||
@ -484,7 +430,7 @@ processJwt_ processAuthZOrCookieHeader_ fGetHeaderType jwtCtxs headers mUnAuthRo
|
||||
where
|
||||
missingAuthzHeader =
|
||||
throw400 InvalidHeaders $
|
||||
"Missing 'Authorization' or 'Cookie' header in JWT authentication mode"
|
||||
"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
|
||||
@ -499,9 +445,6 @@ processAuthZOrCookieHeader ::
|
||||
-- requested Cookie name is not present (returns "m Nothing")
|
||||
m (Maybe (ClaimsMap, Maybe UTCTime))
|
||||
processAuthZOrCookieHeader jwtCtx authzHeader = 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
|
||||
|
@ -13,7 +13,6 @@ import Data.Aeson qualified as J
|
||||
import Data.Aeson.TH qualified as J
|
||||
import Data.ByteString.Char8 (pack, unpack)
|
||||
import Data.FileEmbed (embedStringFile, makeRelativeToProject)
|
||||
import Data.HashMap.Strict qualified as Map
|
||||
import Data.HashSet qualified as Set
|
||||
import Data.String qualified as DataString
|
||||
import Data.Text qualified as T
|
||||
@ -30,7 +29,6 @@ import Hasura.Logging qualified as L
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Auth
|
||||
import Hasura.Server.Auth.JWT
|
||||
import Hasura.Server.Cors
|
||||
import Hasura.Server.Init.Config
|
||||
import Hasura.Server.Logging
|
||||
@ -189,8 +187,7 @@ 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
|
||||
let jwtSecret = maybe mempty (uncurry Map.singleton) $ ((coerce . jcIssuer) &&& id) <$> jwtSecret'
|
||||
jwtSecret <- withEnvJwtConf (rsoJwtSecret rso) $ fst jwtSecretEnv
|
||||
unAuthRole <- withEnv (rsoUnAuthRole rso) $ fst unAuthRoleEnv
|
||||
corsCfg <- mkCorsConfig $ rsoCorsConfig rso
|
||||
enableConsole <-
|
||||
|
@ -44,7 +44,6 @@ import Data.Aeson qualified as J
|
||||
import Data.Aeson.Casing qualified as J
|
||||
import Data.Aeson.TH qualified as J
|
||||
import Data.Char (toLower)
|
||||
import Data.HashMap.Strict qualified as Map
|
||||
import Data.HashSet qualified as Set
|
||||
import Data.String qualified as DataString
|
||||
import Data.Text qualified as T
|
||||
@ -56,7 +55,6 @@ import Hasura.Logging qualified as L
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Auth
|
||||
import Hasura.Server.Auth.JWT
|
||||
import Hasura.Server.Cors
|
||||
import Hasura.Server.Types
|
||||
import Hasura.Server.Utils
|
||||
@ -194,7 +192,7 @@ data ServeOptions impl = ServeOptions
|
||||
soTxIso :: !Q.TxIsolation,
|
||||
soAdminSecret :: !(Set.HashSet AdminSecretHash),
|
||||
soAuthHook :: !(Maybe AuthHook),
|
||||
soJwtSecret :: !(Map.HashMap (Maybe StringOrURI) JWTConfig),
|
||||
soJwtSecret :: !(Maybe JWTConfig),
|
||||
soUnAuthRole :: !(Maybe RoleName),
|
||||
soCorsConfig :: !CorsConfig,
|
||||
soEnableConsole :: !Bool,
|
||||
@ -352,6 +350,9 @@ 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
|
||||
|
||||
|
@ -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 mempty Nothing
|
||||
getUserInfoWithExpTime mempty mempty mode
|
||||
mode <- setupAuthMode'E Nothing Nothing Nothing Nothing
|
||||
getUserInfoWithExpTime mempty [] mode
|
||||
`shouldReturn` Right adminRoleName
|
||||
it "allows any requested role" $ do
|
||||
mode <- setupAuthMode'E Nothing Nothing mempty Nothing
|
||||
mode <- setupAuthMode'E Nothing Nothing Nothing 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 mempty Nothing
|
||||
mode <- runIO $ setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") Nothing Nothing 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 mempty mode
|
||||
getUserInfoWithExpTime 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 mempty (Just ourUnauthRole)
|
||||
setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") Nothing Nothing (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 mempty mode
|
||||
getUserInfoWithExpTime 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) mempty Nothing
|
||||
setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") (Just fakeAuthHook) Nothing 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 mempty mode
|
||||
getUserInfoWithExpTime 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 (uncurry Map.singleton fakeJWTConfig) Nothing
|
||||
setupAuthMode'E (Just $ Set.singleton $ hashAdminSecret "secret") Nothing (Just 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 mempty mode
|
||||
getUserInfoWithExpTime 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
|
||||
(uncurry Map.singleton fakeJWTConfig)
|
||||
(Just 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 mempty mode
|
||||
getUserInfoWithExpTime 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
|
||||
(uncurry Map.singleton fakeJWTConfig)
|
||||
(Just fakeJWTConfig)
|
||||
(Just ourUnauthRole)
|
||||
modeB <-
|
||||
runIO $
|
||||
setupAuthMode'E
|
||||
(Just $ Set.singleton $ hashAdminSecret "secret")
|
||||
Nothing
|
||||
(uncurry Map.singleton fakeJWTConfig)
|
||||
(Just fakeJWTConfig)
|
||||
Nothing
|
||||
|
||||
-- Here the unauth role does not come into play at all, so map same tests over both modes:
|
||||
@ -331,7 +331,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
|
||||
-- A corner case, but the behavior seems desirable:
|
||||
it "always rejects when token has empty allowedRolesClaimText" $ do
|
||||
let claim = unObject [allowedRolesClaimText .= (mempty :: [Text]), defaultRoleClaimText .= ("user" :: Text)]
|
||||
let claim = unObject [allowedRolesClaimText .= ([] :: [Text]), defaultRoleClaimText .= ("user" :: Text)]
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED"), (userRoleHeader, "admin")] mode
|
||||
`shouldReturn` Left AccessDenied
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED"), (userRoleHeader, "user")] mode
|
||||
@ -342,7 +342,7 @@ getUserInfoWithExpTimeTests = describe "getUserInfo" $ do
|
||||
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 mempty
|
||||
claim2 = unObject []
|
||||
for_ [claim0, claim1, claim2] $ \claim ->
|
||||
getUserInfoWithExpTime claim [("Authorization", "IGNORED")] mode
|
||||
`shouldReturn` Left JWTRoleClaimMissing
|
||||
@ -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 mempty Nothing
|
||||
setupAuthMode' Nothing Nothing Nothing Nothing
|
||||
`shouldReturn` Right AMNoAuth
|
||||
-- We insist on an admin secret in order to use webhook or JWT auth:
|
||||
setupAuthMode' Nothing Nothing (uncurry Map.singleton fakeJWTConfig) Nothing
|
||||
setupAuthMode' Nothing Nothing (Just fakeJWTConfig) Nothing
|
||||
`shouldReturn` Left ()
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) mempty Nothing
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) Nothing Nothing
|
||||
`shouldReturn` Left ()
|
||||
-- ...and we can't have both:
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) (uncurry Map.singleton fakeJWTConfig) Nothing
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) (Just 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 mempty (Just unauthRole)
|
||||
setupAuthMode' Nothing Nothing Nothing (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
setupAuthMode' Nothing Nothing (uncurry Map.singleton fakeJWTConfig) (Just unauthRole)
|
||||
setupAuthMode' Nothing Nothing (Just fakeJWTConfig) (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) mempty (Just unauthRole)
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) Nothing (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) (uncurry Map.singleton fakeJWTConfig) (Just unauthRole)
|
||||
setupAuthMode' Nothing (Just fakeAuthHook) (Just fakeJWTConfig) (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
|
||||
it "with admin secret provided" $ do
|
||||
setupAuthMode' (Just secret) Nothing mempty Nothing
|
||||
setupAuthMode' (Just secret) Nothing Nothing Nothing
|
||||
`shouldReturn` Right (AMAdminSecret secret Nothing)
|
||||
setupAuthMode' (Just secret) Nothing mempty (Just unauthRole)
|
||||
setupAuthMode' (Just secret) Nothing Nothing (Just unauthRole)
|
||||
`shouldReturn` Right (AMAdminSecret secret $ Just unauthRole)
|
||||
|
||||
setupAuthMode' (Just secret) Nothing (uncurry Map.singleton fakeJWTConfig) Nothing >>= \case
|
||||
setupAuthMode' (Just secret) Nothing (Just fakeJWTConfig) Nothing >>= \case
|
||||
Right (AMAdminSecretAndJWT s _ Nothing) -> do
|
||||
s `shouldBe` secret
|
||||
_ -> expectationFailure "AMAdminSecretAndJWT"
|
||||
setupAuthMode' (Just secret) Nothing (uncurry Map.singleton fakeJWTConfig) (Just unauthRole) >>= \case
|
||||
setupAuthMode' (Just secret) Nothing (Just fakeJWTConfig) (Just unauthRole) >>= \case
|
||||
Right (AMAdminSecretAndJWT s _ ur) -> do
|
||||
s `shouldBe` secret
|
||||
ur `shouldBe` Just unauthRole
|
||||
_ -> expectationFailure "AMAdminSecretAndJWT"
|
||||
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) mempty Nothing
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) Nothing 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) mempty (Just unauthRole)
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) Nothing (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
-- we can't have both:
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) (uncurry Map.singleton fakeJWTConfig) Nothing
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) (Just fakeJWTConfig) Nothing
|
||||
`shouldReturn` Left ()
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) (uncurry Map.singleton fakeJWTConfig) (Just unauthRole)
|
||||
setupAuthMode' (Just secret) (Just fakeAuthHook) (Just fakeJWTConfig) (Just unauthRole)
|
||||
`shouldReturn` Left ()
|
||||
|
||||
parseClaimsMapTests :: Spec
|
||||
@ -601,7 +601,7 @@ mkCustomOtherClaim claimPath defVal =
|
||||
Just path -> JWTCustomClaimsMapJSONPath (mkJSONPathE path) $ defVal
|
||||
Nothing -> JWTCustomClaimsMapStatic $ fromMaybe "default claim value" defVal
|
||||
|
||||
fakeJWTConfig :: (Maybe StringOrURI, JWTConfig)
|
||||
fakeJWTConfig :: JWTConfig
|
||||
fakeJWTConfig =
|
||||
let jcKeyOrUrl = Left (Jose.fromOctets [])
|
||||
jcAudience = Nothing
|
||||
@ -609,7 +609,7 @@ fakeJWTConfig =
|
||||
jcClaims = JCNamespace (ClaimNs "") JCFJson
|
||||
jcAllowedSkew = Nothing
|
||||
jcHeader = Nothing
|
||||
in (Nothing, JWTConfig {..})
|
||||
in JWTConfig {..}
|
||||
|
||||
fakeAuthHook :: AuthHook
|
||||
fakeAuthHook = AuthHookG "http://fake" AHTGet
|
||||
@ -628,10 +628,10 @@ instance Tracing.HasReporter NoReporter
|
||||
setupAuthMode' ::
|
||||
Maybe (HashSet AdminSecretHash) ->
|
||||
Maybe AuthHook ->
|
||||
Map.HashMap (Maybe StringOrURI) JWTConfig ->
|
||||
Maybe JWTConfig ->
|
||||
Maybe RoleName ->
|
||||
IO (Either () AuthMode)
|
||||
setupAuthMode' mAdminSecretHash mWebHook jwtSecrets mUnAuthRole =
|
||||
setupAuthMode' mAdminSecretHash mWebHook mJwtSecret mUnAuthRole =
|
||||
-- just throw away the error message for ease of testing:
|
||||
fmap (either (const $ Left ()) Right) $
|
||||
runNoReporter $
|
||||
@ -640,7 +640,7 @@ setupAuthMode' mAdminSecretHash mWebHook jwtSecrets mUnAuthRole =
|
||||
setupAuthMode
|
||||
(fromMaybe Set.empty mAdminSecretHash)
|
||||
mWebHook
|
||||
jwtSecrets
|
||||
mJwtSecret
|
||||
mUnAuthRole
|
||||
-- NOTE: this won't do any http or launch threads if we don't specify JWT URL:
|
||||
(error "H.Manager")
|
||||
|
@ -132,7 +132,7 @@ serveOptions =
|
||||
soTxIso = Q.Serializable,
|
||||
soAdminSecret = mempty,
|
||||
soAuthHook = Nothing,
|
||||
soJwtSecret = mempty,
|
||||
soJwtSecret = Nothing,
|
||||
soUnAuthRole = Nothing,
|
||||
soCorsConfig = CCAllowAll,
|
||||
soEnableConsole = True,
|
||||
|
@ -9,7 +9,7 @@ response:
|
||||
- extensions:
|
||||
path: $
|
||||
code: invalid-headers
|
||||
message: Missing 'Authorization' or 'Cookie' header in JWT authentication mode
|
||||
message: Missing 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'] == []
|
||||
assert body['jwt'] == None
|
||||
|
||||
# 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