fradrive/src/Utils/Tokens.hs
Gregor Kleen 98026b2a40 Fix hlint
2019-04-20 22:17:17 +02:00

175 lines
6.1 KiB
Haskell

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
<input type=hidden name=#{toPathPiece PostBearer} value=#{toPathPiece jwt}>
#{fragment}
|]
formEmbedJwtGet f fragment = do
mJwt <- askJwt
f [shamlet|
$newline never
$maybe jwt <- mJwt
<input type=hidden name=#{toPathPiece GetBearer} value=#{toPathPiece jwt}>
#{fragment}
|]