This module doesn’t use any dependency, it is useful if you don’t have any Json library in your project.

It is based a naive parsing of Json strings, and doesn’t support any custom parameter in the Claim so if you need any custom parameter, or if you’re already using one of the supported Json libraries, consider using that instead.

Jwt object

Installation

libraryDependencies += "com.github.jwt-scala" %% "jwt-core" % "9.0.6"

Basic usage

import java.time.Clock
import pdi.jwt.{Jwt, JwtAlgorithm, JwtHeader, JwtClaim, JwtOptions}
implicit val clock: Clock = Clock.systemUTC
// clock: Clock = SystemClock[Z]
val token = Jwt.encode("""{"user":1}""", "secretKey", JwtAlgorithm.HS256)
// token: String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoxfQ.oG3iKnAvj_OKCv0tchT90sv2IFVeaREgvJmwgRcXfkI"
Jwt.decodeRawAll(token, "secretKey", Seq(JwtAlgorithm.HS256))
// res1: util.Try[(String, String, String)] = Success(
//   value = (
//     "{\"typ\":\"JWT\",\"alg\":\"HS256\"}",
//     "{\"user\":1}",
//     "oG3iKnAvj_OKCv0tchT90sv2IFVeaREgvJmwgRcXfkI"
//   )
// )
Jwt.decodeRawAll(token, "wrongKey", Seq(JwtAlgorithm.HS256))
// res2: util.Try[(String, String, String)] = Failure(
//   exception = pdi.jwt.exceptions.JwtValidationException: Invalid signature for this token or wrong algorithm.
// )

Encoding

// Encode from string, header automatically generated
Jwt.encode("""{"user":1}""", "secretKey", JwtAlgorithm.HS384)
// res3: String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJ1c2VyIjoxfQ.Do0PQWccbp1J7ZWcFL-_IY9OFaI-7t75k7-NxZ52jk2kAb0sFopJEeZapkiXthEp"

// Encode from case class, header automatically generated
// Set that the token has been issued now and expires in 10 seconds
Jwt.encode(JwtClaim({"""{"user":1}"""}).issuedNow.expiresIn(10), "secretKey", JwtAlgorithm.HS512)
// res4: String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NTg0NzczMTcsImlhdCI6MTY1ODQ3NzMwNywidXNlciI6MX0.tnUVz-vonSleOYhPrLWP-8ykP_hrgTjdCVQ-IMAXVpKtCbmxsCWHnkyL_dbQ7raytTZgcybj_EjY43Gh8tZTGg"

// You can encode without signing it
Jwt.encode("""{"user":1}""")
// res5: String = "eyJhbGciOiJub25lIn0.eyJ1c2VyIjoxfQ."

// You can specify a string header but also need to specify the algorithm just to be sure
// This is not really typesafe, so please use it with care
Jwt.encode("""{"typ":"JWT","alg":"HS256"}""", """{"user":1}""", "key", JwtAlgorithm.HS256)
// res6: String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoxfQ.kaxGIncoYdxOD5RxfwwiP7mRxqUnRqDemW_f9R1k98U"

// If using a case class header, no need to repeat the algorithm
// This is way better than the previous one
Jwt.encode(JwtHeader(JwtAlgorithm.HS256), JwtClaim("""{"user":1}"""), "key")
// res7: String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoxfQ.kaxGIncoYdxOD5RxfwwiP7mRxqUnRqDemW_f9R1k98U"

Decoding

In JWT Scala, espcially when using raw strings which are not typesafe at all, there are a lot of possible errors. This is why nearly all decode functions will return a Try rather than directly the expected result. In case of failure, the wrapped exception should tell you what went wrong.

Take note that nearly all decoding methods (including those from helper libs) support either a String key, or a PrivateKey with a Hmac algorithm or a PublicKey with a RSA or ECDSA algorithm.

// Decode all parts of the token as string
Jwt.decodeRawAll(token, "secretKey", JwtAlgorithm.allHmac())
// res8: util.Try[(String, String, String)] = Success(
//   value = (
//     "{\"typ\":\"JWT\",\"alg\":\"HS256\"}",
//     "{\"user\":1}",
//     "oG3iKnAvj_OKCv0tchT90sv2IFVeaREgvJmwgRcXfkI"
//   )
// )

// Decode only the claim as a string
Jwt.decodeRaw(token, "secretKey", Seq(JwtAlgorithm.HS256))
// res9: util.Try[String] = Success(value = "{\"user\":1}")

// Decode all parts and cast them as a better type if possible.
// Since the implementation in JWT Core only use string, it is the same as decodeRawAll
// But check the result in JWT Play JSON to see the difference
Jwt.decodeAll(token, "secretKey", Seq(JwtAlgorithm.HS256))
// res10: util.Try[(JwtHeader, JwtClaim, String)] = Success(
//   value = (
//     JwtHeader(Some(HS256), Some(JWT), None, None),
//     JwtClaim({"user":1}, None, None, None, None, None, None, None),
//     "oG3iKnAvj_OKCv0tchT90sv2IFVeaREgvJmwgRcXfkI"
//   )
// )

// Same as before, but only the claim
// (you should start to see a pattern in the naming convention of the functions)
Jwt.decode(token, "secretKey", Seq(JwtAlgorithm.HS256))
// res11: util.Try[JwtClaim] = Success(
//   value = JwtClaim({"user":1}, None, None, None, None, None, None, None)
// )

// Failure because the token is not a token at all
Jwt.decode("Hey there!")
// res12: util.Try[JwtClaim] = Failure(
//   exception = pdi.jwt.exceptions.JwtLengthException: Expected token [Hey there!] to be composed of 2 or 3 parts separated by dots.
// )

// Failure if not Base64 encoded
Jwt.decode("a.b.c")
// res13: util.Try[JwtClaim] = Failure(
//   exception = java.lang.IllegalArgumentException: Input byte[] should at least have 2 bytes for base64 bytes
// )

// Failure in case we use the wrong key
Jwt.decode(token, "wrongKey", Seq(JwtAlgorithm.HS256))
// res14: util.Try[JwtClaim] = Failure(
//   exception = pdi.jwt.exceptions.JwtValidationException: Invalid signature for this token or wrong algorithm.
// )

// Failure if the token only starts in 5 seconds
Jwt.decode(Jwt.encode(JwtClaim().startsIn(5)))
// res15: util.Try[JwtClaim] = Failure(
//   exception = pdi.jwt.exceptions.JwtNotBeforeException: The token will only be valid after 2022-07-22T08:08:32Z
// )

Validating

If you only want to check if a token is valid without decoding it. You have two options: validate functions that will throw the exceptions we saw in the decoding section, so you know what went wrong, or isValid functions that will return a boolean in case you don’t care about the actual error and don’t want to bother with catching exception.

// All good
Jwt.validate(token, "secretKey", Seq(JwtAlgorithm.HS256))
Jwt.isValid(token, "secretKey", Seq(JwtAlgorithm.HS256))

// Wrong key here
Jwt.validate(token, "wrongKey", Seq(JwtAlgorithm.HS256))
Jwt.isValid(token, "wrongKey", Seq(JwtAlgorithm.HS256))

// No key for unsigned token => ok
Jwt.validate(Jwt.encode("{}"))
Jwt.isValid(Jwt.encode("{}"))

// No key while the token is actually signed => wrong
Jwt.validate(token)
Jwt.isValid(token)

// The token hasn't started yet!
Jwt.validate(Jwt.encode(JwtClaim().startsIn(5)))
Jwt.isValid(Jwt.encode(JwtClaim().startsIn(5)))

// This is no token
Jwt.validate("a.b.c")
Jwt.isValid("a.b.c")
// pdi.jwt.exceptions.JwtValidationException: Invalid signature for this token or wrong algorithm.
// 	at pdi.jwt.JwtCore.validate(JwtCore.scala:972)
// 	at pdi.jwt.JwtCore.validate$(JwtCore.scala:949)
// 	at pdi.jwt.Jwt.validate(Jwt.scala:24)
// 	at pdi.jwt.JwtCore.validate(JwtCore.scala:995)
// 	at pdi.jwt.JwtCore.validate$(JwtCore.scala:979)
// 	at pdi.jwt.Jwt.validate(Jwt.scala:24)
// 	at pdi.jwt.JwtCore.validate(JwtCore.scala:1144)
// 	at pdi.jwt.JwtCore.validate$(JwtCore.scala:1129)
// 	at pdi.jwt.Jwt.validate(Jwt.scala:24)
// 	at pdi.jwt.JwtCore.validate(JwtCore.scala:1149)
// 	at pdi.jwt.JwtCore.validate$(JwtCore.scala:1148)
// 	at pdi.jwt.Jwt.validate(Jwt.scala:24)
// 	at repl.MdocSession$App0$$anonfun$22.apply$mcV$sp(jwt-core-jwt.md:88)
// 	at repl.MdocSession$App0$$anonfun$22.apply(jwt-core-jwt.md:88)
// 	at repl.MdocSession$App0$$anonfun$22.apply(jwt-core-jwt.md:88)

Using a custom clock

For testing, it can sometimes be useful to use a fake clock that will always return a fixed time. It can be done by instanciating Jwt instead of using the object (based on the system clock):

import java.time.{Clock, Instant, ZoneId}

val startTime = Clock.fixed(Instant.ofEpochSecond(0), ZoneId.of("UTC"))
// startTime: Clock = FixedClock[1970-01-01T00:00:00Z,UTC]
val endTime = Clock.fixed(Instant.ofEpochSecond(5), ZoneId.of("UTC"))
// endTime: Clock = FixedClock[1970-01-01T00:00:05Z,UTC]

val customJwt = Jwt(endTime)
// customJwt: Jwt = pdi.jwt.Jwt@136e9010

val claim = JwtClaim().issuedNow(startTime).expiresIn(10)(startTime)
// claim: JwtClaim = JwtClaim({}, None, None, None, Some(10), None, Some(0), None)
val encoded = customJwt.encode(claim, "key", JwtAlgorithm.HS256)
// encoded: String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEwLCJpYXQiOjB9.LYTtIU3Vsbt2AGvzk5-769QPqFsSquzYA9RoifdBj68"

customJwt.decode(encoded, "key", JwtAlgorithm.allHmac())
// res16: util.Try[JwtClaim] = Success(
//   value = JwtClaim({}, None, None, None, Some(10), None, Some(0), None)
// )

Options

All validating and decoding methods support a final optional argument as a JwtOptions which allow you to disable validation checks. This is useful if you need to access data from an expired token for example. You can disable expiration, notBefore and signature checks. Be warned that if you disable the last one, you have no guarantee that the user didn’t change the content of the token.

val expiredToken = Jwt.encode(JwtClaim().by("me").expiresIn(-1))
// expiredToken: String = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJtZSIsImV4cCI6MTY1ODQ3NzMwNn0."

// Fail since the token is expired
Jwt.isValid(expiredToken)
// res17: Boolean = false
Jwt.decode(expiredToken)
// res18: util.Try[JwtClaim] = Failure(
//   exception = pdi.jwt.exceptions.JwtExpirationException: The token is expired since 2022-07-22T08:08:26Z
// )

// Let's disable expiration check
Jwt.isValid(expiredToken, JwtOptions(expiration = false))
// res19: Boolean = true
Jwt.decode(expiredToken, JwtOptions(expiration = false))
// res20: util.Try[JwtClaim] = Success(
//   value = JwtClaim({}, Some(me), None, None, Some(1658477306), None, None, None)
// )

You can also specify a leeway, in seconds, to account for clock skew.

// Allow 30sec leeway
Jwt.isValid(expiredToken, JwtOptions(leeway = 30))
// res21: Boolean = true
Jwt.decode(expiredToken, JwtOptions(leeway = 30))
// res22: util.Try[JwtClaim] = Success(
//   value = JwtClaim({}, Some(me), None, None, Some(1658477306), None, None, None)
// )