module Utils.Tokens ( bearerToken , encodeBearer, BearerTokenException(..), decodeBearer , bearerParseJSON' , askBearer , formEmbedBearerPost, formEmbedBearerGet , decodeUploadToken ) where import Import.NoModel import Utils.Lens import Model import Model.Tokens import Jose.Jwk (JwkSet(..)) import qualified Jose.Jwt as Jose import Data.Aeson.Types (Parser) import qualified Data.Aeson as JSON import qualified Data.Aeson.Parser as JSON import qualified Data.Aeson.Parser.Internal as JSON (jsonEOF') import qualified Data.Aeson.Internal as JSON (iparse, formatError) import qualified Data.HashMap.Strict as HashMap import Settings import CryptoID import Text.Blaze (Markup) bearerParseJSON' :: forall m. ( Hashable (AuthId (HandlerSite m)), Eq (AuthId (HandlerSite m)) , HasCryptoUUID (AuthId (HandlerSite m)) (ReaderT CryptoIDKey Parser) , ParseRoute (HandlerSite m) , Hashable (Route (HandlerSite m)) , MonadHandler m , MonadCrypto m , MonadCryptoKey m ~ CryptoIDKey ) => m (Value -> Parser (BearerToken (HandlerSite m))) -- ^ Read `CryptoIDKey` from monadic context and construct a `Parser` for `BearerToken`s bearerParseJSON' = do cidKey <- cryptoIDKey return return $ flip runReaderT cidKey . bearerParseJSON bearerToken :: forall m. ( MonadHandler m , HasInstanceID (HandlerSite m) InstanceId , HasClusterID (HandlerSite m) ClusterId , HasAppSettings (HandlerSite m) ) => HashSet (Either Value (AuthId (HandlerSite m))) -- ^ Authority -> Maybe (AuthId (HandlerSite m)) -- ^ Impersonate -> HashMap BearerTokenRouteMode (HashSet (Route (HandlerSite m))) -> Maybe AuthDNF -- ^ Additional auth -> Maybe (Maybe UTCTime) -- ^ Expiration; @Nothing@ determines default expiry time automatically -> Maybe UTCTime -- ^ Start of Validity; @Nothing@ means token starts to be valid immediately -> m (BearerToken (HandlerSite m)) -- ^ Smart constructor for `BearerToken`, does not set route restrictions (due to polymorphism), use `tokenRestrict` bearerToken bearerAuthority bearerImpersonate bearerRoutes bearerAddAuth mBearerExpiresAt bearerStartsAt = do bearerIdentifier <- liftIO getRandom bearerIssuedAt <- liftIO getCurrentTime bearerIssuedBy <- getsYesod $ view instanceID bearerIssuedFor <- getsYesod $ view clusterID defaultExpiration <- getsYesod $ view _appBearerExpiration let bearerExpiresAt | Just t <- mBearerExpiresAt = t | Just tDiff <- defaultExpiration = Just $ tDiff `addUTCTime` fromMaybe bearerIssuedAt bearerStartsAt | otherwise = Nothing bearerRestrictions = HashMap.empty return BearerToken{..} encodeBearer :: forall m. ( MonadHandler m , HasJSONWebKeySet (HandlerSite m) JwkSet , HasAppSettings (HandlerSite m) , HasCryptoUUID (AuthId (HandlerSite m)) m , RenderRoute (HandlerSite m) ) => BearerToken (HandlerSite m) -> m Jwt -- ^ Call `bearerToJSON` and encode the result as a `Jwt` according to `appJwtEncoding` encodeBearer token = do payload <- Jose.Claims . toStrict . JSON.encode <$> bearerToJSON token JwkSet jwks <- getsYesod $ view jsonWebKeySet jwtEncoding <- getsYesod $ view _appBearerEncoding throwLeft =<< liftIO (Jose.encode jwks jwtEncoding payload) data BearerTokenException = BearerTokenJwtError Jose.JwtError -- ^ An Error occurred in the underlying `Jwt`-Implementation | BearerTokenUnsecured -- ^ `Jwt` is insufficiently secured (unsigned and not encrypted) | BearerTokenInvalidFormat String -- ^ Content of the `Jwt` could not be parsed as a `BearerToken` | BearerTokenExpired | BearerTokenNotStarted | BearerTokenWrongAudience deriving (Eq, Show, Generic, Typeable) deriving anyclass (Exception) decodeBearer :: forall m. ( MonadHandler m , HasJSONWebKeySet (HandlerSite m) JwkSet , Hashable (AuthId (HandlerSite m)), Eq (AuthId (HandlerSite m)) , HasCryptoUUID (AuthId (HandlerSite m)) (ReaderT CryptoIDKey Parser) , MonadCryptoKey m ~ CryptoIDKey , MonadCrypto m , ParseRoute (HandlerSite m) , Hashable (Route (HandlerSite m)) , HasAppSettings (HandlerSite m), HasClusterID (HandlerSite m) ClusterId ) => Jwt -> m (BearerToken (HandlerSite m)) -- ^ Decode a `Jwt` and call `bearerParseJSON` -- -- Throws `BearerTokenException`s decodeBearer (Jwt bs) = do JwkSet jwks <- getsYesod $ view jsonWebKeySet content <- either (throwM . BearerTokenJwtError) return =<< liftIO (Jose.decode jwks Nothing bs) content' <- case content of Jose.Unsecured _ -> throwM BearerTokenUnsecured Jose.Jws (_header, payload) -> return payload Jose.Jwe (_header, payload) -> return payload parser <- bearerParseJSON' bearer@BearerToken{..} <- either (throwM . BearerTokenInvalidFormat . uncurry JSON.formatError) return $ JSON.eitherDecodeStrictWith JSON.jsonEOF' (JSON.iparse parser) content' bearerIssuedFor' <- getsYesod $ view clusterID unless (bearerIssuedFor' == bearerIssuedFor) $ throwM BearerTokenWrongAudience now <- liftIO getCurrentTime (clockLeniencyStart, clockLeniencyEnd) <- getsYesod $ (,) <$> view _appBearerTokenClockLeniencyStart <*> view _appBearerTokenClockLeniencyEnd unless (NTop bearerExpiresAt > NTop (Just $ maybe id addUTCTime (negate <$> clockLeniencyEnd) now)) $ throwM BearerTokenExpired unless (bearerStartsAt <= Just (maybe id addUTCTime clockLeniencyStart now)) $ throwM BearerTokenNotStarted return bearer askBearer :: forall m. ( MonadHandler m ) => m (Maybe Jwt) -- ^ Retrieve current `Jwt` from HTTP-Header, POST-Parameter, or GET-Parameter askBearer = runMaybeT $ asum [ MaybeT lookupBearerAuth >>= hoistMaybe . fromPathPiece , MaybeT $ lookupGlobalPostParam PostBearer , MaybeT $ lookupGlobalGetParam GetBearer , fmap Jwt . MaybeT $ lookupSessionBS (toPathPiece SessionBearer) ] formEmbedBearerPost, formEmbedBearerGet :: MonadHandler m => (Markup -> m a) -> (Markup -> m a) formEmbedBearerPost f fragment = do mBearer <- askBearer f [shamlet| $newline never $maybe bearer <- mBearer #{fragment} |] formEmbedBearerGet f fragment = do mBearer <- askBearer f [shamlet| $newline never $maybe bearer <- mBearer #{fragment} |] data UploadTokenException = UploadTokenJwtError Jose.JwtError -- ^ An Error occurred in the underlying `Jwt`-Implementation | UploadTokenUnsecured -- ^ `Jwt` is insufficiently secured (unsigned and not encrypted) | UploadTokenInvalidFormat String -- ^ Content of the `Jwt` could not be parsed as a `UploadToken` | UploadTokenExpired | UploadTokenNotStarted | UploadTokenWrongAudience deriving (Eq, Show, Generic, Typeable) deriving anyclass (Exception) decodeUploadToken :: forall m. ( MonadHandler m, MonadThrow m , HasJSONWebKeySet (HandlerSite m) JwkSet , HasAppSettings (HandlerSite m), HasClusterID (HandlerSite m) ClusterId ) => Jwt -> m UploadToken -- ^ Decode a `Jwt` -- -- Throws `UploadTokenException`s decodeUploadToken (Jwt bs) = do JwkSet jwks <- getsYesod $ view jsonWebKeySet content <- either (throwM . UploadTokenJwtError) return =<< liftIO (Jose.decode jwks Nothing bs) content' <- case content of Jose.Unsecured _ -> throwM UploadTokenUnsecured Jose.Jws (_header, payload) -> return payload Jose.Jwe (_header, payload) -> return payload uploadToken@UploadToken{..} <- either (throwM . UploadTokenInvalidFormat) return $ JSON.eitherDecodeStrict content' uploadTokenIssuedFor' <- getsYesod $ view clusterID unless (uploadTokenIssuedFor' == uploadTokenIssuedFor) $ throwM UploadTokenWrongAudience now <- liftIO getCurrentTime (clockLeniencyStart, clockLeniencyEnd) <- getsYesod $ (,) <$> view _appUploadTokenClockLeniencyStart <*> view _appUploadTokenClockLeniencyEnd unless (NTop uploadTokenExpiresAt > NTop (Just $ maybe id addUTCTime (negate <$> clockLeniencyEnd) now)) $ throwM UploadTokenExpired unless (uploadTokenStartsAt <= Just (maybe id addUTCTime clockLeniencyStart now)) $ throwM UploadTokenNotStarted return uploadToken