JWT

Our JWT implementation addresses this vulnerability through the scala type system. As all JWTs are typed, both during encoding and decoding, there is no way to parse an arbitrary JWT for an endpoint before knowing the expected algorithm, thus, we avoid the ol’ switcheroo.

The general imports for this module are as follows:

import cats.syntax.all._
import cats.effect.Sync
import tsec.jwt._
import tsec.jws.mac._
import tsec.mac.jca._
import scala.concurrent.duration._

To create JWT Claims, use the JWTClaims object:

scala>   def claimsWithDuration[F[_]: Sync]: F[JWTClaims] = JWTClaims.withDuration[F](expiration = Some(10.minutes))
claimsWithDuration: [F[_]](implicit evidence$1: cats.effect.Sync[F])F[tsec.jwt.JWTClaims]

We can also add custom claims to a JWT object. Imagine we have some custom case class (though it can be any type):

  import io.circe._
  import io.circe.syntax._
  import io.circe.generic.semiauto._

  case class Doge(suchChars: String, much32Bits: Int, so64Bits: Long)
  
  //Note: Normally this would be in the companion
  implicit val encoder: Encoder[Doge]       = deriveEncoder[Doge]
  implicit val decoder: Decoder[Doge]       = deriveDecoder[Doge]
  val WowSuchClaim                          = "Doge"

We can add it via explicit json serialization as such:

scala>   JWTClaims(customFields = Seq(WowSuchClaim -> Doge("w00f", 8008135, 80085L).asJson))
res1: tsec.jwt.JWTClaims =
JWTClaims(None,None,None,None,None,None,Some(9525bdae5d20a28aaf52e47a2bbbd6e4),object[iss -> null,sub -> null,aud -> null,exp -> null,nbf -> null,iat -> null,jti -> "9525bdae5d20a28aaf52e47a2bbbd6e4",Doge -> {
  "suchChars" : "w00f",
  "much32Bits" : 8008135,
  "so64Bits" : 80085
}])

The JWT module comes with two ways to work with JWT by default interpreting into a Target F[_] with a Sync[F], or interpreting into Either[Throwable, A] if you do not like writing pure code.

  /** You can interpret into any target Monad with an instance of Sync[F] using JwtMac */
  def jwtMonadic[F[_]: Sync]: F[JWTMac[HMACSHA256]] =
    for {
      key             <- HMACSHA256.generateKey[F]
      claims          <- JWTClaims.withDuration[F](expiration = Some(10.minutes))
      jwt             <- JWTMac.build[F, HMACSHA256](claims, key) //You can sign and build a jwt object directly
      verifiedFromObj <- JWTMac.verifyFromInstance[F, HMACSHA256](jwt, key) //Verify from an object directly
      stringjwt       <- JWTMac.buildToString[F, HMACSHA256](claims, key) //Or build it straight to string
      isverified      <- JWTMac.verifyFromString[F, HMACSHA256](stringjwt, key) //You can verify straight from a string
      parsed          <- JWTMac.verifyAndParse[F, HMACSHA256](stringjwt, key) //Or verify and return the actual instance
    } yield parsed
    
  import java.time.Instant

  /** Or using an impure either interpreter */
  val impureClaims = JWTClaims(expiration = Some(Instant.now.plusSeconds(10.minutes.toSeconds)))

  val jwt: Either[Throwable, JWTMac[HMACSHA256]] = for {
    key             <- HMACSHA256.generateKey[MacErrorM]
    jwt             <- JWTMacImpure.build[HMACSHA256](impureClaims, key) //You can sign and build a jwt object directly
    verifiedFromObj <- JWTMacImpure.verifyFromInstance[HMACSHA256](jwt, key)
    stringjwt       <- JWTMacImpure.buildToString[HMACSHA256](impureClaims, key) //Or build it straight to string
    isverified      <- JWTMacImpure.verifyFromString[HMACSHA256](stringjwt, key) //You can verify straight from a string
    parsed          <- JWTMacImpure.verifyAndParse[HMACSHA256](stringjwt, key) //Or verify and return the actual instance
  } yield parsed