218 lines
8.6 KiB
Haskell
218 lines
8.6 KiB
Haskell
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
|
|
--
|
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
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 site m.
|
|
( Hashable (AuthId site), Eq (AuthId site)
|
|
, HasCryptoUUID (AuthId site) (ReaderT CryptoIDKey Parser)
|
|
, ParseRoute site
|
|
, Hashable (Route site)
|
|
, MonadSite site m
|
|
, MonadCrypto m
|
|
, MonadCryptoKey m ~ CryptoIDKey
|
|
)
|
|
=> m (Value -> Parser (BearerToken site))
|
|
-- ^ Read `CryptoIDKey` from monadic context and construct a `Parser` for `BearerToken`s
|
|
bearerParseJSON' = do
|
|
cidKey <- cryptoIDKey return
|
|
return $ flip runReaderT cidKey . bearerParseJSON
|
|
|
|
|
|
bearerToken :: forall site m.
|
|
( MonadSite site m
|
|
, MonadIO m
|
|
, HasInstanceID site InstanceId
|
|
, HasClusterID site ClusterId
|
|
, HasAppSettings site
|
|
)
|
|
=> HashSet (Either Value (AuthId site)) -- ^ Authority
|
|
-> Maybe (AuthId site) -- ^ Impersonate
|
|
-> HashMap BearerTokenRouteMode (HashSet (Route site))
|
|
-> 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 site)
|
|
-- ^ 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 <- getsSite $ view instanceID
|
|
bearerIssuedFor <- getsSite $ view clusterID
|
|
|
|
defaultExpiration <- getsSite $ 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 site m.
|
|
( MonadSite site m
|
|
, MonadIO m
|
|
, HasJSONWebKeySet site JwkSet
|
|
, HasAppSettings site
|
|
, HasCryptoUUID (AuthId site) m
|
|
, RenderRoute site
|
|
)
|
|
=> BearerToken site -> 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 <- getsSite $ view jsonWebKeySet
|
|
jwtEncoding <- getsSite $ 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)
|
|
deriving anyclass (Exception)
|
|
|
|
decodeBearer :: forall site m.
|
|
( MonadSite site m
|
|
, MonadIO m
|
|
, HasJSONWebKeySet site JwkSet
|
|
, Hashable (AuthId site), Eq (AuthId site)
|
|
, HasCryptoUUID (AuthId site) (ReaderT CryptoIDKey Parser)
|
|
, MonadCryptoKey m ~ CryptoIDKey
|
|
, MonadCrypto m
|
|
, ParseRoute site
|
|
, Hashable (Route site)
|
|
, HasAppSettings site, HasClusterID site ClusterId
|
|
)
|
|
=> Jwt -> m (BearerToken site)
|
|
-- ^ Decode a `Jwt` and call `bearerParseJSON`
|
|
--
|
|
-- Throws `BearerTokenException`s
|
|
decodeBearer (Jwt bs) = do
|
|
JwkSet jwks <- getsSite $ 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' <- getsSite $ view clusterID
|
|
unless (bearerIssuedFor' == bearerIssuedFor) $
|
|
throwM BearerTokenWrongAudience
|
|
now <- liftIO getCurrentTime
|
|
(clockLeniencyStart, clockLeniencyEnd) <- getsSite $ (,) <$> 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
|
|
<input type=hidden name=#{toPathPiece PostBearer} value=#{toPathPiece bearer}>
|
|
#{fragment}
|
|
|]
|
|
formEmbedBearerGet f fragment = do
|
|
mBearer <- askBearer
|
|
f [shamlet|
|
|
$newline never
|
|
$maybe bearer <- mBearer
|
|
<input type=hidden name=#{toPathPiece GetBearer} value=#{toPathPiece bearer}>
|
|
#{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)
|
|
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
|