JWT Scala

AuthenticationJWTScalaSecurityTokenPlay Framework

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

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]"
    }
  }
}