---
title: "Add JWT Authentication to Your Play Framework API"
description: "Secure your Play Framework API using JWT authentication with JWKS."
canonicalUrl: "https://zuplo.com/use-cases/api-key-auth/scalajava/playframework/jwt-backend"
framework: "Play Framework"
language: "Scala"
authStrategy: "JWT with JWKS"
pageType: use-case
---

# Add JWT Authentication to Your Play Framework API

Secure your Play Framework API using JWT authentication with JWKS.

## How Zuplo Handles It

Let Zuplo issue short-lived JWTs signed with a JWKS your Play Framework backend can verify — no long-lived API keys touch your origin.

## Play Framework Backend Code

```scala
// app/filters/JwtAuthFilter.scala
package filters

import javax.inject.Inject
import akka.stream.Materializer
import play.api.mvc._
import play.api.libs.json._
import play.api.libs.ws.WSClient
import pdi.jwt.{JwtJson, JwtAlgorithm, JwtClaim}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Success, Failure}

class JwtAuthFilter @Inject()(
  implicit val mat: Materializer,
  ec: ExecutionContext,
  ws: WSClient
) extends Filter {

  private val issuer = "https://my-api-a32f34.zuplo.api/__zuplo/issuer"
  private val jwksUri = s"$issuer/.well-known/jwks.json"

  @volatile private var cachedJwks: Option[(JsValue, Long)] = None

  def apply(nextFilter: RequestHeader => Future[Result])(request: RequestHeader): Future[Result] = {
    request.headers.get("Authorization") match {
      case Some(header) if header.startsWith("Bearer ") =>
        val token = header.drop(7)
        validateToken(token).flatMap {
          case Right(claims) =>
            nextFilter(request.addAttr(JwtAuthFilter.UserKey, claims))
          case Left(error) =>
            Future.successful(Results.Unauthorized(Json.obj("error" -> error)))
        }
      case _ =>
        Future.successful(Results.Unauthorized(Json.obj("error" -> "No token provided")))
    }
  }

  private def validateToken(token: String): Future[Either[String, JsValue]] = {
    fetchJwks().map { jwks =>
      JwtJson.decodeJson(token, jwks, Seq(JwtAlgorithm.RS256)) match {
        case Success(claims) =>
          val claimsJson = Json.parse(claims.toJson)
          if ((claimsJson \ "iss").asOpt[String].contains(issuer)) {
            Right(claimsJson)
          } else {
            Left("Invalid issuer")
          }
        case Failure(e) =>
          Left(s"Invalid token: ${e.getMessage}")
      }
    }
  }

  private def fetchJwks(): Future[String] = {
    val now = System.currentTimeMillis()
    cachedJwks match {
      case Some((jwks, timestamp)) if now - timestamp < 600000 =>
        Future.successful(jwks.toString)
      case _ =>
        ws.url(jwksUri).get().map { response =>
          cachedJwks = Some((response.json, now))
          response.json.toString
        }
    }
  }
}

object JwtAuthFilter {
  val UserKey = TypedKey[JsValue]("user")
}

// app/controllers/ProtectedController.scala
package controllers

import javax.inject.Inject
import play.api.mvc._
import play.api.libs.json._
import filters.JwtAuthFilter

class ProtectedController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {

  def index = Action { request =>
    request.attrs.get(JwtAuthFilter.UserKey) match {
      case Some(user) =>
        Ok(Json.obj(
          "message" -> "Access granted",
          "user" -> user
        ))
      case None =>
        Unauthorized(Json.obj("error" -> "Not authenticated"))
    }
  }
}

// conf/routes
// GET /protected controllers.ProtectedController.index
```

## Example Request

```bash
curl -X GET \
  'https://your-api.zuplo.dev/your-route' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

## Learn More

- [API Key Authentication on Zuplo](https://zuplo.com/docs/policies/api-key-auth-inbound)
- [JWT Authentication on Zuplo](https://zuplo.com/docs/policies/open-id-jwt-auth-inbound)
- [All use cases](https://zuplo.com/use-cases)
