module Utils.Tokens ( bearerToken , encodeToken, BearerTokenException(..), decodeToken , tokenParseJSON' , askJwt , formEmbedJwtPost, formEmbedJwtGet ) where import ClassyPrelude.Yesod import Yesod.Auth (AuthId) import Utils (NTop(..), hoistMaybe, SessionKey(..)) import Utils.Parameters import Utils.Lens import Model import Model.Tokens import Jose.Jwk (JwkSet(..)) import Jose.Jwt (Jwt(..)) 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 Data.Time.Clock import Control.Monad.Random (MonadRandom(..)) import Control.Monad.Trans.Maybe (MaybeT(..)) import Settings import CryptoID import Text.Blaze (Markup) tokenParseJSON' :: forall 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 tokenParseJSON' = do cidKey <- cryptoIDKey return return $ flip runReaderT cidKey . tokenParseJSON bearerToken :: forall m. ( MonadHandler m , HasInstanceID (HandlerSite m) InstanceId , HasCryptoUUID (AuthId (HandlerSite m)) m , HasAppSettings (HandlerSite m) ) => AuthId (HandlerSite m) -> Maybe (HashSet (Route (HandlerSite m))) -> Maybe AuthDNF -> Maybe (Maybe UTCTime) -- ^ @Nothing@ determines default expiry time automatically -> Maybe UTCTime -- ^ @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 tokenAuthority tokenRoutes tokenAddAuth mTokenExpiresAt tokenStartsAt = do tokenIdentifier <- liftIO getRandom tokenIssuedAt <- liftIO getCurrentTime tokenIssuedBy <- getsYesod $ view instanceID defaultExpiration <- getsYesod $ view _appJwtExpiration let tokenExpiresAt | Just t <- mTokenExpiresAt = t | Just tDiff <- defaultExpiration = Just $ tDiff `addUTCTime` fromMaybe tokenIssuedAt tokenStartsAt | otherwise = Nothing tokenRestrictions = HashMap.empty return BearerToken{..} encodeToken :: forall m. ( MonadHandler m , HasJSONWebKeySet (HandlerSite m) JwkSet , HasInstanceID (HandlerSite m) InstanceId , HasAppSettings (HandlerSite m) , HasCryptoUUID (AuthId (HandlerSite m)) m , RenderRoute (HandlerSite m) ) => BearerToken (HandlerSite m) -> m Jwt -- ^ Call `tokenToJSON` and encode the result as a `Jwt` according to `appJwtEncoding` encodeToken token = do payload <- Jose.Claims . toStrict . JSON.encode <$> tokenToJSON token JwkSet jwks <- getsYesod $ view jsonWebKeySet jwtEncoding <- getsYesod $ view _appJwtEncoding either throwM return =<< 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 deriving (Eq, Show, Generic, Typeable) instance Exception BearerTokenException decodeToken :: forall m. ( MonadHandler m , HasJSONWebKeySet (HandlerSite m) JwkSet , HasCryptoUUID (AuthId (HandlerSite m)) (ReaderT CryptoIDKey Parser) , MonadCryptoKey m ~ CryptoIDKey , MonadCrypto m , MonadThrow m , ParseRoute (HandlerSite m) , Hashable (Route (HandlerSite m)) ) => Jwt -> m (BearerToken (HandlerSite m)) -- ^ Decode a `Jwt` and call `tokenParseJSON` -- -- Throws `bearerTokenException`s decodeToken (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 <- tokenParseJSON' token@BearerToken{..} <- either (throwM . BearerTokenInvalidFormat . uncurry JSON.formatError) return $ JSON.eitherDecodeStrictWith JSON.jsonEOF' (JSON.iparse parser) content' now <- liftIO getCurrentTime unless (NTop tokenExpiresAt > NTop (Just now)) $ throwM BearerTokenExpired unless (tokenStartsAt <= Just now) $ throwM BearerTokenNotStarted return token askJwt :: forall m. ( MonadHandler m ) => m (Maybe Jwt) -- ^ Retrieve current `Jwt` from HTTP-Header, POST-Parameter, or GET-Parameter askJwt = runMaybeT $ asum [ MaybeT lookupBearerAuth >>= hoistMaybe . fromPathPiece , MaybeT $ lookupGlobalPostParam PostBearer , MaybeT $ lookupGlobalGetParam GetBearer , fmap Jwt . MaybeT $ lookupSessionBS (toPathPiece SessionBearer) ] formEmbedJwtPost, formEmbedJwtGet :: MonadHandler m => (Markup -> m a) -> (Markup -> m a) formEmbedJwtPost f fragment = do mJwt <- askJwt f [shamlet| $newline never $maybe jwt <- mJwt #{fragment} |] formEmbedJwtGet f fragment = do mJwt <- askJwt f [shamlet| $newline never $maybe jwt <- mJwt #{fragment} |]