175 lines
6.1 KiB
Haskell
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}
|
|
|]
|