JWT Scala
Library
JWT Scala
Overview
JWT Scala is a JSON Web Token (JWT) library for Scala applications, supporting integration with Play Framework, JSON4s, Circe, and other popular libraries.
Details
jwt-scala is a comprehensive JWT library supporting Scala 2.12, 2.13, and Scala 3. It provides a dependency-free core library and includes extension modules for popular JSON libraries such as Play Framework, Play JSON, Json4s Native, Json4s Jackson, Circe, uPickle, and Argonaut. Supporting Java 8+, it implements standard signing algorithms including HMAC, RSA, and ECDSA. The project is divided into multiple sub-projects, allowing selection of appropriate modules based on the required JSON library. Detailed documentation is provided on the Microsite, making it a mature library suitable for enterprise applications.
Pros and Cons
Pros
- Multi-Library Support: Integration support for major JSON libraries
- Dependency-Free: Core library has no external dependencies
- Full Scala Support: Compatible with Scala 2.12, 2.13, and Scala 3
- Play Framework Integration: Natural integration with Play applications
- Modular Design: Select only required functionality
- Type Safety: Enhanced safety through Scala's type system
Cons
- Scala-Specific: Cannot be used with non-Scala languages
- Learning Curve: Requires understanding of Scala ecosystem
- Configuration Complexity: JSON library selection and setup
- Documentation: Limited information for some JSON library integrations
- Performance: JVM execution overhead
Main Links
- GitHub - jwt-scala/jwt-scala
- JWT Scala Official Site
- Maven Repository
- Circe Integration Documentation
- JWT.io
- Scala Official Site
Code Examples
Basic JWT Operations (Core Library)
import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim, JwtHeader}
import java.time.Instant
// Secret key
val secret = "mySecretKey"
// Create claim
val claim = JwtClaim(
content = """{"user_id": 123, "email": "[email protected]"}""",
issuedAt = Some(Instant.now.getEpochSecond),
expiration = Some(Instant.now.plusSeconds(3600).getEpochSecond)
)
// Generate JWT token
val token = Jwt.encode(claim, secret, JwtAlgorithm.HS256)
println(s"Generated token: $token")
// Decode and verify token
Jwt.decode(token, secret, Seq(JwtAlgorithm.HS256)) match {
case Success(claims) =>
println(s"Successfully decoded: $claims")
case Failure(exception) =>
println(s"Failed to decode: ${exception.getMessage}")
}
Play JSON Integration
import play.api.libs.json._
import pdi.jwt.{JwtJson, JwtAlgorithm, JwtClaim}
// User data case class
case class User(id: Long, email: String, role: String)
object User {
implicit val userFormat: Format[User] = Json.format[User]
}
// JWT operations with Play JSON
object JwtService {
private val secret = "mySecretKey"
private val algorithm = JwtAlgorithm.HS256
def createToken(user: User): String = {
val claim = JwtClaim(
content = Json.toJson(user).toString,
issuedAt = Some(Instant.now.getEpochSecond),
expiration = Some(Instant.now.plusSeconds(3600).getEpochSecond)
)
JwtJson.encode(claim, secret, algorithm)
}
def validateToken(token: String): Option[User] = {
JwtJson.decode(token, secret, Seq(algorithm)) match {
case Success(claims) =>
Json.parse(claims.content).validate[User] match {
case JsSuccess(user, _) => Some(user)
case JsError(_) => None
}
case Failure(_) => None
}
}
}
// Usage example
val user = User(123, "[email protected]", "admin")
val token = JwtService.createToken(user)
val decodedUser = JwtService.validateToken(token)
Circe Integration
import io.circe._
import io.circe.generic.auto._
import io.circe.parser._
import io.circe.syntax._
import pdi.jwt.{JwtCirce, JwtAlgorithm, JwtClaim}
// User data
case class UserClaims(
userId: Long,
email: String,
role: String,
permissions: List[String]
)
object CirceJwtService {
private val secret = "mySecretKey"
private val algorithm = JwtAlgorithm.HS256
def encode(userClaims: UserClaims): String = {
val claim = JwtClaim(
content = userClaims.asJson.noSpaces,
issuedAt = Some(Instant.now.getEpochSecond),
expiration = Some(Instant.now.plusSeconds(3600).getEpochSecond)
)
JwtCirce.encode(claim, secret, algorithm)
}
def decode(token: String): Either[Error, UserClaims] = {
JwtCirce.decode(token, secret, Seq(algorithm)) match {
case Success(claims) =>
parse(claims.content).flatMap(_.as[UserClaims])
case Failure(exception) =>
Left(new Error(s"JWT decode failed: ${exception.getMessage}"))
}
}
}
// Usage example
val userClaims = UserClaims(
userId = 456,
email = "[email protected]",
role = "admin",
permissions = List("read", "write", "delete")
)
val token = CirceJwtService.encode(userClaims)
CirceJwtService.decode(token) match {
case Right(claims) => println(s"Decoded successfully: $claims")
case Left(error) => println(s"Error: ${error.getMessage}")
}
Authentication in Play Framework Application
// app/controllers/AuthController.scala
import javax.inject._
import play.api.mvc._
import play.api.libs.json._
import scala.concurrent.{ExecutionContext, Future}
@Singleton
class AuthController @Inject()(
val controllerComponents: ControllerComponents
)(implicit ec: ExecutionContext) extends BaseController {
def login = Action.async(parse.json) { request =>
request.body.validate[LoginRequest] match {
case JsSuccess(loginReq, _) =>
// User authentication logic (simplified)
authenticateUser(loginReq.email, loginReq.password).map {
case Some(user) =>
val token = JwtService.createToken(user)
Ok(Json.obj(
"token" -> token,
"user" -> Json.toJson(user)
))
case None =>
Unauthorized(Json.obj("error" -> "Invalid credentials"))
}
case JsError(errors) =>
Future.successful(BadRequest(Json.obj("error" -> "Invalid request")))
}
}
def me = AuthenticatedAction { request =>
Ok(Json.toJson(request.user))
}
private def authenticateUser(email: String, password: String): Future[Option[User]] = {
// Retrieve user from database and verify password
// In actual implementation, use proper encryption and database access
Future.successful(Some(User(1, email, "user")))
}
}
// Custom authentication action
case class AuthenticatedRequest[A](user: User, request: Request[A]) extends WrappedRequest[A](request)
class AuthenticatedAction @Inject()(
parser: BodyParsers.Default
)(implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: AuthenticatedRequest[A] => Future[Result]): Future[Result] = {
request.headers.get("Authorization") match {
case Some(authHeader) if authHeader.startsWith("Bearer ") =>
val token = authHeader.substring(7)
JwtService.validateToken(token) match {
case Some(user) =>
block(AuthenticatedRequest(user, request))
case None =>
Future.successful(Results.Unauthorized(Json.obj("error" -> "Invalid token")))
}
case _ =>
Future.successful(Results.Unauthorized(Json.obj("error" -> "Missing authorization header")))
}
}
}
Permission Management System
// models/Permission.scala
sealed trait Permission
object Permission {
case object ReadPosts extends Permission
case object WritePosts extends Permission
case object DeletePosts extends Permission
case object ManageUsers extends Permission
implicit val permissionFormat: Format[Permission] = new Format[Permission] {
def reads(json: JsValue): JsResult[Permission] = json.as[String] match {
case "read_posts" => JsSuccess(ReadPosts)
case "write_posts" => JsSuccess(WritePosts)
case "delete_posts" => JsSuccess(DeletePosts)
case "manage_users" => JsSuccess(ManageUsers)
case other => JsError(s"Unknown permission: $other")
}
def writes(permission: Permission): JsValue = JsString(permission match {
case ReadPosts => "read_posts"
case WritePosts => "write_posts"
case DeletePosts => "delete_posts"
case ManageUsers => "manage_users"
})
}
}
// User with permissions
case class AuthorizedUser(
id: Long,
email: String,
role: String,
permissions: Set[Permission]
) {
def hasPermission(permission: Permission): Boolean = {
role == "admin" || permissions.contains(permission)
}
}
// Permission-checking action
class AuthorizedAction(requiredPermission: Permission) @Inject()(
parser: BodyParsers.Default
)(implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: Request[A] => Future[Result]): Future[Result] = {
// Extract permissions from JWT token and check
extractUserFromToken(request) match {
case Some(user) if user.hasPermission(requiredPermission) =>
block(request)
case Some(_) =>
Future.successful(Results.Forbidden(Json.obj("error" -> "Insufficient permissions")))
case None =>
Future.successful(Results.Unauthorized(Json.obj("error" -> "Authentication required")))
}
}
private def extractUserFromToken(request: Request[_]): Option[AuthorizedUser] = {
// Extract AuthorizedUser from JWT token
// Implementation details omitted
None
}
}
JWT Configuration and Middleware
// app/filters/JwtFilter.scala
import akka.stream.Materializer
import javax.inject._
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}
@Singleton
class JwtFilter @Inject()(
implicit override val mat: Materializer,
exec: ExecutionContext
) extends Filter {
override def apply(nextFilter: RequestHeader => Future[Result])
(requestHeader: RequestHeader): Future[Result] = {
// Skip paths that don't require authentication
if (!requiresAuthentication(requestHeader.path)) {
return nextFilter(requestHeader)
}
requestHeader.headers.get("Authorization") match {
case Some(authHeader) if authHeader.startsWith("Bearer ") =>
val token = authHeader.substring(7)
JwtService.validateToken(token) match {
case Some(user) =>
// Add user information to request
val enrichedHeader = requestHeader.withAttrs(
requestHeader.attrs + (Attrs.User -> user)
)
nextFilter(enrichedHeader)
case None =>
Future.successful(Results.Unauthorized(Json.obj("error" -> "Invalid token")))
}
case _ =>
Future.successful(Results.Unauthorized(Json.obj("error" -> "Missing authorization")))
}
}
private def requiresAuthentication(path: String): Boolean = {
val publicPaths = Set("/login", "/register", "/health", "/")
!publicPaths.contains(path) && !path.startsWith("/assets")
}
}
// Request attribute definitions
object Attrs {
val User = TypedKey[User]("user")
}
// application.conf
play.filters.enabled += "filters.JwtFilter"
JWT Usage in Tests
// test/controllers/AuthControllerSpec.scala
import org.scalatestplus.play._
import org.scalatestplus.play.guice._
import play.api.test._
import play.api.test.Helpers._
import play.api.libs.json._
class AuthControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {
"AuthController" should {
"authenticate valid user" in {
val request = FakeRequest(POST, "/auth/login")
.withJsonBody(Json.obj(
"email" -> "[email protected]",
"password" -> "password123"
))
val result = route(app, request).get
status(result) mustBe OK
(contentAsJson(result) \ "token").asOpt[String] mustBe defined
}
"reject invalid credentials" in {
val request = FakeRequest(POST, "/auth/login")
.withJsonBody(Json.obj(
"email" -> "[email protected]",
"password" -> "wrongpassword"
))
val result = route(app, request).get
status(result) mustBe UNAUTHORIZED
}
"return user info for valid token" in {
val user = User(1, "[email protected]", "user")
val token = JwtService.createToken(user)
val request = FakeRequest(GET, "/auth/me")
.withHeaders("Authorization" -> s"Bearer $token")
val result = route(app, request).get
status(result) mustBe OK
(contentAsJson(result) \ "email").as[String] mustBe "[email protected]"
}
}
}