JwtSession case class

Installation

libraryDependencies += "com.github.jwt-scala" %% "jwt-play" % "9.1.1"

Provides an API similar to the Play Session but using JsValue rather than String as values. It also separates headerData from claimData rather than having only one data.

Basic usage

import java.time.Clock
import pdi.jwt.JwtSession
import play.api.Configuration

implicit val clock: Clock = Clock.systemUTC
// clock: Clock = SystemClock[Z]

//In a real Play! App this should normally be injected in the constructor with @Inject()
implicit val conf: Configuration = Configuration.reference
// conf: Configuration = Configuration(
//   underlying = Config(SimpleConfigObject({"akka":{"actor":{"allow-java-serialization":"off","creation-timeout":"20s","debug":{"autoreceive":"off","event-stream":"off","fsm":"off","lifecycle":"off","receive":"off","router-misconfiguration":"off","unhandled":"off"},"default-blocking-io-dispatcher":{"executor":"thread-pool-executor","thread-pool-executor":{"fixed-pool-size":16},"throughput":1,"type":"Dispatcher"},"default-dispatcher":{"affinity-pool-executor":{"fair-work-distribution":{"threshold":128},"idle-cpu-level":5,"parallelism-factor":0.8,"parallelism-max":64,"parallelism-min":4,"queue-selector":"akka.dispatch.affinity.FairDistributionHashCache","rejection-handler":"akka.dispatch.affinity.ThrowOnOverflowRejectionHandler","task-queue-size":512},"attempt-teamwork":"on","default-executor":{"fallback":"fork-join-executor"},"executor":"default-executor","fork-join-executor":{"parallelism-factor":1,"parallelism-max":64,"parallelism-min":8,"task-peeking-mode":"FIFO"},"mailbox-requirement":"","shutdown-timeout":"1s","thread-pool-executor":{"allow-core-timeout":"on","core-pool-size-factor":3,"core-pool-size-max":64,"core-pool-size-min":8,"fixed-pool-size":"off","keep-alive-time":"60s","max-pool-size-factor":3,"max-pool-size-max":64,"max-pool-size-min":8,"task-queue-size":-1,"task-queue-type":"linked"},"throughput":5,"throughput-deadline-time":"0ms","type":"Dispatcher"},"default-mailbox":{"mailbox-capacity":1000,"mailbox-push-timeout-time":"10s","mailbox-type":"akka.dispatch.UnboundedMailbox","stash-capacity":-1},"deployment":{"/IO-DNS/async-dns":{"mailbox":"unbounded","nr-of-instances":1,"router":"round-robin-pool"},"/IO-DNS/inet-address":{"mailbox":"unbounded","nr-of-instances":4,"router":"consistent-hashing-pool"},"/IO-DNS/inet-address/*":{"dispatcher":"akka.actor.default-blocking-io-dispatcher"},"default":{"dispatcher":"","mailbox":"","nr-of-instances":1,"optimal-size-exploring-resizer":{"action-interval":"5s","chance-of-exploration":0.4,"chance-of-ramping-down-when-full":0.2,"downsize-after-underutilized-for":"72h","downsize-ratio":0.8,"enabled":"off","explore-step-size":0.1,"lower-bound":1,"optimization-range":16,"upper-bound":10,"weight-of-latest-metric":0.5},"resizer":{"backoff-rate":0.1,"backoff-threshold":0.3,"enabled":"off","lower-bound":1,"messages-per-resize":10,"pressure-threshold":1,"rampup-rate":0.2,"upper-bound":10},"routees":{"paths":[]},"router":"from-code","tail-chopping-router":{"interval":"10 milliseconds"},"virtual-nodes-factor":10,"within":"5 seconds"}},"guardian-supervisor-strategy":"akka.actor.DefaultSupervisorStrategy","internal-dispatcher":{"executor":"fork-join-executor","fork-join-executor":{"parallelism-factor":1,"parallelism-max":64,"parallelism-min":4},"throughput":5,"type":"Dispatcher"},"mailbox":{"bounded-control-aware-queue-based":{"mailbox-type":"akka.dispatch.BoundedControlAwareMailbox"},"bounded-deque-based":{"mailbox-type":"akka.dispatch.BoundedDequeBasedMailbox"},"bounded-queue-based":{"mailbox-type":"akka.dispatch.BoundedMailbox"},"logger-queue":{"mailbox-type":"akka.event.LoggerMailboxType"},"requirements":{"akka.dispatch.BoundedControlAwareMessageQueueSemantics":"akka.actor.mailbox.bounded-control-aware-queue-based","akka.dispatch.BoundedDequeBasedMessageQueueSemantics":"akka.actor.mailbox.bounded-deque-based","akka.dispatch.BoundedMessageQueueSemantics":"akka.actor.mailbox.bounded-queue-based","akka.dispatch.ControlAwareMessageQueueSemantics":"akka.actor.mailbox.unbounded-control-aware-queue-based","akka.dispatch.DequeBasedMessageQueueSemantics":"akka.actor.mailbox.unbounded-deque-based","akka.dispatch.MultipleConsumerSemantics":"akka.actor.mailbox.unbounded-queue-based","akka.dispatch.UnboundedControlAwareMessageQueueSemantics":"akka.actor.mailbox.unbounded-control-aware-queue-based","akka.dispatch.UnboundedDequeBasedMessageQueueS...

// Let's create a session, it will automatically assign a default header. No
// In your app, the default header would be generated from "application.conf" file
// but here, it will just use the default values (which are all empty)
var session = JwtSession()
// session: JwtSession = JwtSession(
//   headerData = JsObject(
//     underlying = Map(
//       "typ" -> JsString(value = "JWT"),
//       "alg" -> JsString(value = "HS256")
//     )
//   ),
//   claimData = JsObject(underlying = Map()),
//   signature = ""
// )

// We can add a (key, value)
session = session + ("user", 1)

// Or several of them
session = session ++ (("nbf", 1431520421), ("key", "value"), ("key2", 2), ("key3", 3))

// Also remove a key
session = session - "key"

// Or several
session = session -- ("key2", "key3")

// We can access a specific key
session.get("user")
// res4: Option[play.api.libs.json.JsValue] = Some(value = JsNumber(value = 1))

// Test if the session is empty or not
// (it is not here since we have several keys in the claimData)
session.isEmpty()
// res5: Boolean = false

// Serializing the session is the same as encoding it as a JSON Web Token
val token = session.serialize
// token: String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE0MzE1MjA0MjEsInVzZXIiOjF9.0nOuCxfVTiJEOwB99ruZ0a5iP5HumhG5xu0ccKoonSM"

// You can create a JwtSession from a token of course
JwtSession.deserialize(token)
// res6: JwtSession = JwtSession(
//   headerData = JsObject(
//     underlying = Map(
//       "typ" -> JsString(value = "JWT"),
//       "alg" -> JsString(value = "HS256")
//     )
//   ),
//   claimData = JsObject(
//     underlying = Map(
//       "nbf" -> JsNumber(value = 1431520421),
//       "user" -> JsNumber(value = 1)
//     )
//   ),
//   signature = "0nOuCxfVTiJEOwB99ruZ0a5iP5HumhG5xu0ccKoonSM"
// )

// You could refresh the session to set its expiration in a few seconds from now
// but you need to set "session.maxAge" in your "application.conf" and since this
// is not a real Play application, we cannot do that, so here, the refresh will do nothing.
session = session.refresh()

Using implicits

If you have implicit Reads and/or Writes, you can access and/or add data directly as case class or object.

// First, creating the implicits
import play.api.libs.json.Json
import pdi.jwt.JwtSession

case class User(id: Long, name: String)
implicit val formatUser = Json.format[User]
// formatUser: play.api.libs.json.OFormat[User] = play.api.libs.json.OFormat$$anon$3@315fa9ff

// Next, adding it to a new session
val session2 = JwtSession() + ("user", User(42, "Paul"))
// session2: JwtSession = JwtSession(
//   headerData = JsObject(
//     underlying = Map(
//       "typ" -> JsString(value = "JWT"),
//       "alg" -> JsString(value = "HS256")
//     )
//   ),
//   claimData = JsObject(
//     underlying = HashMap(
//       "user" -> JsObject(
//         underlying = Map(
//           "id" -> JsNumber(value = 42),
//           "name" -> JsString(value = "Paul")
//         )
//       )
//     )
//   ),
//   signature = ""
// )

// Finally, accessing it
session2.getAs[User]("user")
// res8: Option[User] = Some(value = User(id = 42L, name = "Paul"))

Play RequestHeader

You can extract a JwtSession from a RequestHeader.

import pdi.jwt.JwtSession._
import play.api.test.FakeRequest

import play.api.Configuration

// Default JwtSession
FakeRequest().jwtSession
// res9: JwtSession = JwtSession(
//   headerData = JsObject(
//     underlying = Map(
//       "typ" -> JsString(value = "JWT"),
//       "alg" -> JsString(value = "HS256")
//     )
//   ),
//   claimData = JsObject(underlying = Map()),
//   signature = ""
// )

// What about some headers?
// (the default header for a JSON Web Token is "Authorization" and it should be prefixed by "Bearer ")
val request = FakeRequest().withHeaders(("Authorization", "Bearer " + session2.serialize))
// request: FakeRequest[play.api.mvc.AnyContentAsEmpty.type] = GET /
request.jwtSession
// res10: JwtSession = JwtSession(
//   headerData = JsObject(
//     underlying = Map(
//       "typ" -> JsString(value = "JWT"),
//       "alg" -> JsString(value = "HS256")
//     )
//   ),
//   claimData = JsObject(
//     underlying = Map(
//       "user" -> JsObject(
//         underlying = Map(
//           "id" -> JsNumber(value = 42),
//           "name" -> JsString(value = "Paul")
//         )
//       )
//     )
//   ),
//   signature = "P8vGvJMv8aTU8vxiP5b1U-IHjZpFpGid46UEMszKW2I"
// )

// It means you can directly read case classes from the session!
// And that's pretty cool
request.jwtSession.getAs[User]("user")
// res11: Option[User] = Some(value = User(id = 42L, name = "Paul"))

Play Result

There are also implicit helpers around Result to help you manipulate the session inside it.

implicit val implRequest = request
// implRequest: FakeRequest[play.api.mvc.AnyContentAsEmpty.type] = GET /

// Let's begin by creating a Result
var result: play.api.mvc.Result = play.api.mvc.Results.Ok
// result: play.api.mvc.Result = Result(
//   header = 200, TreeMap(),
//   body = Strict(data = ByteString(), contentType = None),
//   newSession = None,
//   newFlash = None,
//   newCookies = List()
// )

// We can already get a JwtSession from our implicit RequestHeader
result.jwtSession
// res12: JwtSession = JwtSession(
//   headerData = JsObject(
//     underlying = Map(
//       "typ" -> JsString(value = "JWT"),
//       "alg" -> JsString(value = "HS256")
//     )
//   ),
//   claimData = JsObject(
//     underlying = Map(
//       "user" -> JsObject(
//         underlying = Map(
//           "id" -> JsNumber(value = 42),
//           "name" -> JsString(value = "Paul")
//         )
//       )
//     )
//   ),
//   signature = "P8vGvJMv8aTU8vxiP5b1U-IHjZpFpGid46UEMszKW2I"
// )

// Setting a new empty JwtSession
result = result.withNewJwtSession

// Or from an existing JwtSession
result = result.withJwtSession(session2)

// Or from a JsObject
result = result.withJwtSession(Json.obj(("id", 1), ("key", "value")))

// Or from (key, value)
result = result.withJwtSession(("id", 1), ("key", "value"))

// We can add stuff to the current session (only (String, String))
result = result.addingToJwtSession(("key2", "value2"), ("key3", "value3"))

// Or directly classes or objects if you have the correct implicit Writes
result = result.addingToJwtSession("user", User(1, "Paul"))

// Removing from session
result = result.removingFromJwtSession("key2", "key3")

// Refresh the current session
result = result.refreshJwtSession

// So, at the end, you can do
result.jwtSession.getAs[User]("user")
// res21: Option[User] = Some(value = User(id = 1L, name = "Paul"))

Play configuration

Secret key

play.http.secret.key

Default: none

The secret key is used to secure cryptographics functions. We are using the same key to sign Json Web Tokens so you don’t need to worry about it.

Private key

play.http.session.privateKey

Default: none

The PKCS8 format private key is used to sign JWT session. If play.http.session.privateKey is missing play.http.secret.key used instead.

Public key

play.http.session.publicKey

Default: none

The X.509 format public key is used to verify JWT session signed with private key play.http.session.privateKey

Session timeout

play.http.session.maxAge

Default: none

Just like for the cookie session, you can use this key to specify the duration, in milliseconds or using the duration syntax (for example 30m or 1h), after which the user should be logout, which mean the token will no longer be valid. It means you need to refresh the expiration date at each request

Signature algorithm

play.http.session.algorithm

Default: HS256

Supported: HMD5, HS1, HS224, HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512

You can specify which algorithm you want to use, among the supported ones, in order to create the signature which will assure you that nobody can actually change the token. You should probably stick with the default one or use HmacSHA512 for maximum security.

Header name

play.http.session.jwtName

Default: Authorization

You can change the name of the header in which the token should be stored. It will be used for both requests and responses.

Response header name

play.http.session.jwtResponseName

Default: none

If you need to have a different header for request and response, you can override the response header using this key.

Token prefix

play.http.session.tokenPrefix

Default: “Bearer “

Authorization header should have a prefix before the token, like “Basic” for example. For a JWT token, it should be “Bearer” (which is the default value) but you can freely change or remove it (using an empty string). The token prefix will be directly prepend before the token, so be sure to put any necessary whitespaces in it.