{-# LANGUAGE PackageImports #-} module Token (Token, generate, resolve) where import qualified Prelude as P import qualified Data.Either as Either import qualified Data.Maybe as HMaybe import qualified Data.Aeson.Types as Json import qualified Data.Aeson as Aeson import qualified "text" Data.Text as T import qualified "text" Data.Text.Lazy as TL import qualified "text" Data.Text.Encoding as Encoding import qualified "text" Data.Text.Encoding.Error as Encoding import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Base64 as Base64 import qualified Data.Time.Clock.POSIX as POSIX import qualified Data.Time.Clock as Clock import qualified Data.Maybe as HMaybe import qualified Jose.Jwt as Jwt import qualified Jose.Jwa as Jwa import qualified Control.Monad.Except as Except import qualified Text.Read import qualified Internal.Shortcut as Shortcut import qualified Interop import qualified JWT import qualified Result import qualified Task import qualified Internal.Task import qualified Maybe import qualified String import qualified Time import Cherry.Prelude {-| -} type Token = String -- GENERATE {-| -} generate :: JWT.Env -> String -> Task String Token generate env userId = do now <- Time.now payload <- encodeJwt env (claims env userId now) Task.succeed (String.fromByteString payload) claims :: JWT.Env -> String -> Time.POSIX -> Jwt.Payload claims env userId now = Jwt.JwtClaims { Jwt.jwtIss = HMaybe.Nothing , Jwt.jwtSub = HMaybe.Just (T.pack (String.toList userId)) , Jwt.jwtAud = HMaybe.Nothing , Jwt.jwtExp = HMaybe.Just (Jwt.IntDate (expirationTime env now)) , Jwt.jwtNbf = HMaybe.Nothing , Jwt.jwtIat = HMaybe.Nothing , Jwt.jwtJti = HMaybe.Nothing } |> Aeson.encode |> BL.toStrict |> Jwt.Claims expirationTime :: JWT.Env -> Time.POSIX -> Time.POSIX expirationTime env now = now + P.fromIntegral (JWT.expirationSecs env) encodeJwt :: JWT.Env -> Jwt.Payload -> Task String B.ByteString encodeJwt env payload = let fromEither either = case either of Either.Right (Jwt.Jwt encoded) -> Result.Ok encoded Either.Left err -> Result.Err (toErrorMsg err) in Jwt.encode (JWT.jwks env) (Jwt.JwsEncoding Jwa.RS256) payload |> Shortcut.map fromEither |> Internal.Task.Task -- RESOLVE {-| -} resolve :: JWT.Env -> Token -> Task String String resolve env token = let check :: Time.POSIX -> B.ByteString -> Task String String check now token_ = decodeClaims token_ |> Task.andThen (isExpired now) |> Task.andThen parseClaims in do now <- Time.now token_ <- decodeToken env token check now token_ decodeToken :: JWT.Env -> Token -> Task String B.ByteString decodeToken env token = let utf8Token = String.toByteString token settings = Jwt.JwsEncoding Jwa.RS256 fromEither jwt = case jwt of Either.Right ( Jwt.Jws ( header, json ) ) -> Ok json Either.Left err -> Err (toErrorMsg err) in Jwt.decode (JWT.jwks env) (HMaybe.Just settings) utf8Token |> Shortcut.map fromEither |> Internal.Task.Task decodeClaims :: B.ByteString -> Task String Jwt.JwtClaims decodeClaims claims = claims |> BL.fromStrict |> Aeson.eitherDecode |> Result.fromEither |> Result.mapError String.fromList |> Task.fromResult isExpired :: Time.POSIX -> Jwt.JwtClaims -> Task String Jwt.JwtClaims isExpired now claims = if expiration now claims < now then Task.fail "Token is expired." else Task.succeed claims expiration :: Time.POSIX -> Jwt.JwtClaims -> Time.POSIX expiration now claims = let jwtExpiration = Maybe.fromHMaybe (Jwt.jwtExp claims) (Jwt.IntDate date) = Maybe.withDefault (Jwt.IntDate now) jwtExpiration in date parseClaims :: Jwt.JwtClaims -> Task String String parseClaims claims = claims |> Jwt.jwtSub |> Maybe.fromHMaybe |> Maybe.map (String.fromByteString << Encoding.encodeUtf8) |> Result.fromMaybe "Could not parse claims." |> Task.fromResult -- SHARED toErrorMsg :: Jwt.JwtError -> String toErrorMsg error = case error of -- TODO err type ? Jwt.KeyError _ -> "No suitable key or wrong key type." Jwt.BadAlgorithm _ -> "The supplied algorithm is invalid." Jwt.BadDots _ -> "Wrong number of \".\" characters in the JWT." Jwt.BadHeader _ -> "Header couldn't be decoded or contains bad data." Jwt.BadClaims -> "Claims part couldn't be decoded or contains bad data." Jwt.BadSignature -> "Signature is invalid." Jwt.BadCrypto -> "A cryptographic operation failed." Jwt.Base64Error _ -> "A base64 decoding error."