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:
Anon Ray 2021-12-14 19:58:50 +05:30 committed by hasura-bot
parent 5726852c54
commit 4121c1dd3d
13 changed files with 85 additions and 204 deletions

View File

@ -31,4 +31,3 @@ Features
api-limits
disable-graphql-introspection
rotating-admin-secrets
multiple-jwt-secrets

View File

@ -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.

View File

@ -156,7 +156,6 @@ library
, mtl
, openapi3
, optparse-applicative
, optparse-generic
, parsec
, pg-client
, postgresql-binary

View File

@ -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.
--

View File

@ -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

View File

@ -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 =

View File

@ -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

View File

@ -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 <-

View File

@ -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

View File

@ -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")

View File

@ -132,7 +132,7 @@ serveOptions =
soTxIso = Q.Serializable,
soAdminSecret = mempty,
soAuthHook = Nothing,
soJwtSecret = mempty,
soJwtSecret = Nothing,
soUnAuthRole = Nothing,
soCorsConfig = CCAllowAll,
soEnableConsole = True,

View File

@ -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 {

View File

@ -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: