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

# Add JWT Authentication to Your Lagom API

Secure your Lagom API using JWT authentication with JWKS.

## How Zuplo Handles It

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

## Lagom Backend Code

```scala
// api/src/main/scala/com/example/api/ExampleService.scala
package com.example.api

import akka.NotUsed
import com.lightbend.lagom.scaladsl.api.{Service, ServiceCall}
import com.lightbend.lagom.scaladsl.api.transport.Method

trait ExampleService extends Service {
  def protectedEndpoint: ServiceCall[NotUsed, ProtectedResponse]

  override def descriptor = {
    import Service._
    named("example")
      .withCalls(
        restCall(Method.GET, "/protected", protectedEndpoint _)
      )
      .withAutoAcl(true)
  }
}

case class ProtectedResponse(message: String, user: Map[String, String])

// impl/src/main/scala/com/example/impl/JwtAuthenticator.scala
package com.example.impl

import com.lightbend.lagom.scaladsl.api.transport.{Forbidden, RequestHeader}
import com.lightbend.lagom.scaladsl.server.ServerServiceCall
import pdi.jwt.{JwtJson, JwtAlgorithm}
import play.api.libs.json._
import play.api.libs.ws.WSClient
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Success, Failure}

class JwtAuthenticator(ws: WSClient)(implicit ec: ExecutionContext) {

  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 authenticated[Request, Response](
    serviceCall: JsValue => ServerServiceCall[Request, Response]
  ): ServerServiceCall[Request, Response] = {
    ServerServiceCall.compose { requestHeader =>
      extractAndValidateToken(requestHeader).map { claims =>
        serviceCall(claims)
      }
    }
  }

  private def extractAndValidateToken(header: RequestHeader): Future[JsValue] = {
    header.getHeader("Authorization") match {
      case Some(auth) if auth.startsWith("Bearer ") =>
        val token = auth.drop(7)
        validateToken(token)
      case _ =>
        Future.failed(Forbidden("No token provided"))
    }
  }

  private def validateToken(token: String): Future[JsValue] = {
    fetchJwks().flatMap { 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)) {
            Future.successful(claimsJson)
          } else {
            Future.failed(Forbidden("Invalid issuer"))
          }
        case Failure(e) =>
          Future.failed(Forbidden(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
        }
    }
  }
}

// impl/src/main/scala/com/example/impl/ExampleServiceImpl.scala
package com.example.impl

import akka.NotUsed
import com.example.api.{ExampleService, ProtectedResponse}
import com.lightbend.lagom.scaladsl.api.ServiceCall
import play.api.libs.json.JsValue
import scala.concurrent.{ExecutionContext, Future}

class ExampleServiceImpl(authenticator: JwtAuthenticator)(implicit ec: ExecutionContext)
  extends ExampleService {

  override def protectedEndpoint: ServiceCall[NotUsed, ProtectedResponse] =
    authenticator.authenticated { claims =>
      ServerServiceCall { _ =>
        Future.successful(ProtectedResponse(
          message = "Access granted",
          user = Map(
            "sub" -> (claims \ "sub").asOpt[String].getOrElse(""),
            "iss" -> (claims \ "iss").asOpt[String].getOrElse("")
          )
        ))
      }
    }
}
```

## 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)
