refactor: Encapsulate aud config
This change is an initial step to change JWT aud configuration to regular expression. Exporting function audMatchesCfg :: AppConfig -> Text -> Bool from Config module allows changing the way how JWT aud is configured to be isolated and not affect code in Auth.JWT
This commit is contained in:
committed by
Laurence Isla
parent
0f7ac1bc39
commit
5a4e2e4dec
+12
-14
@@ -35,7 +35,7 @@ import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
|||||||
|
|
||||||
import PostgREST.Auth.Types (AuthResult (..))
|
import PostgREST.Auth.Types (AuthResult (..))
|
||||||
import PostgREST.Config (AppConfig (..), FilterExp (..), JSPath,
|
import PostgREST.Config (AppConfig (..), FilterExp (..), JSPath,
|
||||||
JSPathExp (..))
|
JSPathExp (..), audMatchesCfg)
|
||||||
import PostgREST.Error (Error (..),
|
import PostgREST.Error (Error (..),
|
||||||
JwtClaimsError (AudClaimNotStringOrArray, ExpClaimNotNumber, IatClaimNotNumber, JWTExpired, JWTIssuedAtFuture, JWTNotInAudience, JWTNotYetValid, NbfClaimNotNumber, ParsingClaimsFailed),
|
JwtClaimsError (AudClaimNotStringOrArray, ExpClaimNotNumber, IatClaimNotNumber, JWTExpired, JWTIssuedAtFuture, JWTNotInAudience, JWTNotYetValid, NbfClaimNotNumber, ParsingClaimsFailed),
|
||||||
JwtDecodeError (..), JwtError (..))
|
JwtDecodeError (..), JwtError (..))
|
||||||
@@ -52,21 +52,21 @@ decodeClaims :: MonadError Error m => JWT.JwtContent -> m JSON.Object
|
|||||||
decodeClaims (JWT.Jws (_, claims)) = maybe (throwError (JwtErr $ JwtClaimsErr ParsingClaimsFailed)) pure (JSON.decodeStrict claims)
|
decodeClaims (JWT.Jws (_, claims)) = maybe (throwError (JwtErr $ JwtClaimsErr ParsingClaimsFailed)) pure (JSON.decodeStrict claims)
|
||||||
decodeClaims _ = throwError $ JwtErr $ JwtDecodeErr UnsupportedTokenType
|
decodeClaims _ = throwError $ JwtErr $ JwtDecodeErr UnsupportedTokenType
|
||||||
|
|
||||||
validateClaims :: MonadError Error m => UTCTime -> Maybe Text -> JSON.Object -> m ()
|
validateClaims :: MonadError Error m => UTCTime -> (Text -> Bool) -> JSON.Object -> m ()
|
||||||
validateClaims time getConfigAud claims = liftEither $ maybeToLeft () (fmap JwtErr . getAlt $ JwtClaimsErr <$> checkForErrors time getConfigAud claims)
|
validateClaims time audMatches claims = liftEither $ maybeToLeft () (fmap JwtErr . getAlt $ JwtClaimsErr <$> checkForErrors time audMatches claims)
|
||||||
|
|
||||||
data ValidAud = VANull | VAString Text | VAArray [Text] deriving Generic
|
data ValidAud = VANull | VAString Text | VAArray [Text] deriving Generic
|
||||||
instance JSON.FromJSON ValidAud where
|
instance JSON.FromJSON ValidAud where
|
||||||
parseJSON JSON.Null = pure VANull
|
parseJSON JSON.Null = pure VANull
|
||||||
parseJSON o = JSON.genericParseJSON JSON.defaultOptions { JSON.sumEncoding = JSON.UntaggedValue } o
|
parseJSON o = JSON.genericParseJSON JSON.defaultOptions { JSON.sumEncoding = JSON.UntaggedValue } o
|
||||||
|
|
||||||
checkForErrors :: (Monad m, forall a. Monoid (m a)) => UTCTime -> Maybe Text -> JSON.Object -> m JwtClaimsError
|
checkForErrors :: (Applicative m, Monoid (m JwtClaimsError)) => UTCTime -> (Text -> Bool) -> JSON.Object -> m JwtClaimsError
|
||||||
checkForErrors time cfgAud = mconcat
|
checkForErrors time audMatches = mconcat
|
||||||
[
|
[
|
||||||
claim "exp" ExpClaimNotNumber $ inThePast JWTExpired
|
claim "exp" ExpClaimNotNumber $ inThePast JWTExpired
|
||||||
, claim "nbf" NbfClaimNotNumber $ inTheFuture JWTNotYetValid
|
, claim "nbf" NbfClaimNotNumber $ inTheFuture JWTNotYetValid
|
||||||
, claim "iat" IatClaimNotNumber $ inTheFuture JWTIssuedAtFuture
|
, claim "iat" IatClaimNotNumber $ inTheFuture JWTIssuedAtFuture
|
||||||
, claim "aud" AudClaimNotStringOrArray checkAud
|
, claim "aud" AudClaimNotStringOrArray $ checkValue (not . validAud) JWTNotInAudience
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
allowedSkewSeconds = 30 :: Int64
|
allowedSkewSeconds = 30 :: Int64
|
||||||
@@ -79,12 +79,10 @@ checkForErrors time cfgAud = mconcat
|
|||||||
|
|
||||||
checkTime cond = checkValue (cond. sciToInt)
|
checkTime cond = checkValue (cond. sciToInt)
|
||||||
|
|
||||||
checkAud = \case
|
validAud = \case
|
||||||
(VAString aud) -> liftMaybe cfgAud >>= checkValue (aud /=) JWTNotInAudience
|
(VAString aud) -> audMatches aud
|
||||||
(VAArray auds) | (not . null) auds -> liftMaybe cfgAud >>= checkValue (not . (`elem` auds)) JWTNotInAudience
|
(VAArray auds) -> null auds || any audMatches auds
|
||||||
_ -> mempty
|
_ -> True
|
||||||
|
|
||||||
liftMaybe = maybe mempty pure
|
|
||||||
|
|
||||||
checkValue invalid msg val =
|
checkValue invalid msg val =
|
||||||
if invalid val then
|
if invalid val then
|
||||||
@@ -122,8 +120,8 @@ parseToken secret tkn = do
|
|||||||
jwtDecodeError _ = JwtDecodeErr UnreachableDecodeError
|
jwtDecodeError _ = JwtDecodeErr UnreachableDecodeError
|
||||||
|
|
||||||
parseClaims :: (MonadError Error m, MonadIO m) => AppConfig -> UTCTime -> JSON.Object -> m AuthResult
|
parseClaims :: (MonadError Error m, MonadIO m) => AppConfig -> UTCTime -> JSON.Object -> m AuthResult
|
||||||
parseClaims AppConfig{configJwtAudience, configJwtRoleClaimKey, configDbAnonRole} time mclaims = do
|
parseClaims cfg@AppConfig{configJwtRoleClaimKey, configDbAnonRole} time mclaims = do
|
||||||
validateClaims time configJwtAudience mclaims
|
validateClaims time (audMatchesCfg cfg) mclaims
|
||||||
-- role defaults to anon if not specified in jwt
|
-- role defaults to anon if not specified in jwt
|
||||||
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
|
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
|
||||||
unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ module PostgREST.Config
|
|||||||
, addFallbackAppName
|
, addFallbackAppName
|
||||||
, addTargetSessionAttrs
|
, addTargetSessionAttrs
|
||||||
, exampleConfigFile
|
, exampleConfigFile
|
||||||
|
, audMatchesCfg
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
@@ -67,6 +68,8 @@ import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
|
|||||||
|
|
||||||
import Protolude hiding (Proxy, toList)
|
import Protolude hiding (Proxy, toList)
|
||||||
|
|
||||||
|
audMatchesCfg :: AppConfig -> Text -> Bool
|
||||||
|
audMatchesCfg = maybe (const True) (==) . configJwtAudience
|
||||||
|
|
||||||
data AppConfig = AppConfig
|
data AppConfig = AppConfig
|
||||||
{ configAppSettings :: [(Text, Text)]
|
{ configAppSettings :: [(Text, Text)]
|
||||||
|
|||||||
Reference in New Issue
Block a user