{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# LANGUAGE UndecidableInstances #-} module Model.Tokens.Bearer ( BearerToken(..), BearerTokenRouteMode(..) , _bearerIdentifier, _bearerAuthority, _bearerRoutes, _bearerAddAuth, _bearerRestrictions, _bearerRestrictionIx, _bearerRestrictionAt, _bearerIssuedAt, _bearerIssuedBy, _bearerExpiresAt, _bearerStartsAt , bearerRestrict , bearerToJSON, bearerParseJSON ) where import ClassyPrelude.Yesod import Yesod.Core.Instances () import Yesod.Servant (MonadSite(..)) import Model import Model.Tokens.Lens import Utils (assertM', foldMapM) import Utils.Lens hiding ((.=)) import Data.Aeson.Lens (AsJSON(..)) import Utils.PathPiece import Data.Universe import Yesod.Auth (AuthId) import Jose.Jwt (IntDate(..)) import qualified Jose.Jwt as Jose import Jose.Jwt.Instances () import qualified Data.HashSet as HashSet import qualified Data.HashMap.Strict as HashMap import Data.Time.Clock.Instances () import Data.Aeson.Types (Parser, (.:?), (.!=)) import qualified Data.Aeson as JSON import qualified Data.Aeson.Types as JSON import CryptoID import Data.Time.Clock.POSIX import Data.Binary (Binary) import qualified Data.CryptoID.Class.ImplicitNamespace as I data BearerTokenRouteMode = BearerTokenRouteEval -- ^ Token is not to be evaluated for routes outside of the given restriction | BearerTokenRouteAccess -- ^ Token may be evaluated for routes outside of the given restriction, but not if the initial request was outside the restriction deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving anyclass (Universe, Finite, Hashable, Binary) nullaryPathPiece ''BearerTokenRouteMode $ camelToPathPiece' 3 pathPieceJSON ''BearerTokenRouteMode pathPieceJSONKey ''BearerTokenRouteMode instance Default BearerTokenRouteMode where def = BearerTokenRouteEval -- | Presenting a `BearerToken` transfers some authorisation from `tokenAuthority` to /whoever/ presents the token data BearerToken site = BearerToken { bearerIdentifier :: TokenId -- ^ Unique identifier for each token; maybe useful for tracing usage of tokens , bearerAuthority :: HashSet (Either Value (AuthId site)) -- ^ Tokens only grant rights the `bearerAuthority` has (i.e. `AuthTag`s are evaluated with the user set to `bearerAuthority`) , bearerImpersonate :: Maybe (AuthId site) -- ^ Token doubles as session token; i.e. if presented `maybeAuthId` etc. should evaluate to the given value , bearerRoutes :: HashMap BearerTokenRouteMode (HashSet (Route site)) -- ^ Tokens can optionally be restricted to only be usable on a subset of routes , bearerAddAuth :: Maybe AuthDNF -- ^ Tokens can specify an additional predicate logic formula of `AuthTag`s that needs to evaluate to `Authorized` in order for the token to be valid. , bearerRestrictions :: HashMap (Route site) Value -- ^ Tokens can be restricted to certain actions within the context of a route (i.e. creating an user only with a certain username, resetting a password only if the old matches a hash, ...) -- -- In general this is not encrypted; some care is required to not expose sensitive information to the bearer of the token , bearerIssuedAt :: UTCTime , bearerIssuedBy :: InstanceId , bearerIssuedFor :: ClusterId , bearerExpiresAt , bearerStartsAt :: Maybe UTCTime } deriving (Generic, Typeable) deriving stock instance (Eq (AuthId site), Eq (Route site)) => Eq (BearerToken site) deriving stock instance (Ord (AuthId site), Ord (Route site)) => Ord (BearerToken site) deriving stock instance (Read (AuthId site), Eq (Route site), Hashable (Route site), Read (Route site), Hashable (AuthId site), Eq (AuthId site)) => Read (BearerToken site) deriving stock instance (Show (AuthId site), Show (Route site), Hashable (AuthId site)) => Show (BearerToken site) deriving anyclass instance (Hashable (AuthId site), Hashable (Route site)) => Hashable (BearerToken site) deriving anyclass instance (Binary (AuthId site), Binary (Route site), Hashable (Route site), Eq (Route site), Hashable (AuthId site), Eq (AuthId site)) => Binary (BearerToken site) makeLenses_ ''BearerToken instance HasTokenIdentifier (BearerToken site) TokenId where _tokenIdentifier = _bearerIdentifier instance HasTokenIssuedBy (BearerToken site) InstanceId where _tokenIssuedBy = _bearerIssuedBy instance HasTokenIssuedFor (BearerToken site) ClusterId where _tokenIssuedFor = _bearerIssuedFor instance HasTokenIssuedAt (BearerToken site) UTCTime where _tokenIssuedAt = _bearerIssuedAt instance HasTokenExpiresAt (BearerToken site) (Maybe UTCTime) where _tokenExpiresAt = _bearerExpiresAt instance HasTokenStartsAt (BearerToken site) (Maybe UTCTime) where _tokenStartsAt = _bearerStartsAt _bearerRestrictionIx :: (FromJSON a, ToJSON a, Hashable (Route site), Eq (Route site)) => Route site -> Traversal' (BearerToken site) a -- ^ Focus a singular restriction (by route) if it exists -- -- This /cannot/ be used to add restrictions, use `_bearerRestrictionAt` or `bearerRestrict` instead _bearerRestrictionIx route = _bearerRestrictions . ix route . _JSON _bearerRestrictionAt :: (FromJSON a, ToJSON a, Hashable (Route site), Eq (Route site)) => Route site -> Traversal' (BearerToken site) (Maybe a) -- ^ Focus a singular restriction (by route) whether it exists, or not _bearerRestrictionAt route = _bearerRestrictions . at route . maybePrism _JSON bearerRestrict :: (ToJSON a, Hashable (Route site), Eq (Route site)) => Route site -> a -> BearerToken site -> BearerToken site -- ^ Add a restriction to a `BearerToken` -- -- If a restriction already exists for the targeted route, it's silently overwritten bearerRestrict route (toJSON -> resVal) = over _bearerRestrictions $ HashMap.insert route resVal bearerToJSON :: forall site m. ( MonadSite site m , HasCryptoUUID (AuthId site) m , RenderRoute site ) => BearerToken site -> m Value -- ^ Encode a `BearerToken` analogously to `toJSON` -- -- Monadic context is needed because `AuthId`s are encrypted during encoding bearerToJSON BearerToken{..} = do cID <- foldMapM (fmap HashSet.singleton . either (return . Left) (fmap Right . I.encrypt)) bearerAuthority :: m (HashSet (Either Value (CryptoUUID (AuthId site)))) let stdPayload = Jose.JwtClaims { jwtIss = Just $ toPathPiece bearerIssuedBy , jwtSub = Nothing , jwtAud = Just . pure $ toPathPiece bearerIssuedFor , jwtExp = IntDate . utcTimeToPOSIXSeconds <$> bearerExpiresAt , jwtNbf = IntDate . utcTimeToPOSIXSeconds <$> bearerStartsAt , jwtIat = Just . IntDate $ utcTimeToPOSIXSeconds bearerIssuedAt , jwtJti = Just $ toPathPiece bearerIdentifier } authorityToJSON auths | [auth] <- otoList auths = either toJSON toJSON auth | otherwise = toJSON $ HashSet.map (either toJSON toJSON) auths iCID <- traverse I.encrypt bearerImpersonate :: m (Maybe (CryptoUUID (AuthId site))) return . JSON.object $ catMaybes [ Just $ "authority" .= authorityToJSON cID , ("impersonate" .=) <$> iCID , ("routes" .=) <$> assertM' (not . HashMap.null) bearerRoutes , ("add-auth" .=) <$> bearerAddAuth , ("restrictions" .=) <$> assertM' (not . HashMap.null) bearerRestrictions ] ++ let JSON.Object hm = toJSON stdPayload in HashMap.toList hm bearerParseJSON :: forall site. ( Hashable (AuthId site), Eq (AuthId site) , HasCryptoUUID (AuthId site) (ReaderT CryptoIDKey Parser) , ParseRoute site , Hashable (Route site) ) => Value -> ReaderT CryptoIDKey Parser (BearerToken site) -- ^ Decode a `Value` to a `BearerToken` analogously to `parseJSON` -- -- Monadic context is needed because `AuthId`s are encrypted during encoding -- -- It's usually easier to use `Utils.Tokens.bearerParseJSON'` bearerParseJSON v@(Object o) = do bearerAuthority' <- lift $ asum [ HashSet.singleton . Right <$> o .: "authority" , (o .: "authority" :: Parser (HashSet Value)) >>= foldMapM (\v' -> fmap HashSet.singleton $ (Right <$> parseJSON v') <|> return (Left v')) , HashSet.singleton . Left <$> o .: "authority" ] :: ReaderT CryptoIDKey Parser (HashSet (Either Value (CryptoUUID (AuthId site)))) bearerAuthority <- foldMapM (fmap HashSet.singleton . either (return . Left) (fmap Right . I.decrypt)) bearerAuthority' bearerImpersonate <- traverse I.decrypt <=< lift $ (o .:? "impersonate" :: Parser (Maybe (CryptoUUID (AuthId site)))) bearerRoutes <- lift $ (o .:? "routes" .!= HashMap.empty) <|> (maybe HashMap.empty (HashMap.singleton def) <$> o .:? "routes") bearerAddAuth <- lift $ o .:? "add-auth" bearerRestrictions <- lift $ o .:? "restrictions" .!= HashMap.empty Jose.JwtClaims{..} <- lift $ parseJSON v let unIntDate (IntDate posix) = posixSecondsToUTCTime posix Just bearerIssuedBy <- return $ jwtIss >>= fromPathPiece Just bearerIssuedFor <- return $ do [aud] <- jwtAud fromPathPiece aud Just bearerIdentifier <- return $ jwtJti >>= fromPathPiece Just bearerIssuedAt <- return $ unIntDate <$> jwtIat let bearerExpiresAt = unIntDate <$> jwtExp bearerStartsAt = unIntDate <$> jwtNbf return BearerToken{..} bearerParseJSON v = lift $ JSON.typeMismatch "BearerToken" v