Skip to Content
Course content

173: JWT Authentication in Play/Akka HTTP

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How do I actually create the token once a user logs in?

Once you've verified the user's credentials against your database, you don't want to keep checking the password on every single request. That's where the JWT comes in. I usually reach for the jwt-scala library because it's lightweight and doesn't bring in a mountain of dependencies.

The key is to pack a "claim" into the token—something that identifies the user, like their userId—and set a reasonable expiration time. If you make the token last for a year, you've just created a security hole. I typically go with 15 minutes to an hour.

import jwt.JwtScala
import jwt.JwtScala.Claims

val secret = "your-super-secret-key-keep-it-safe" // In production, load this from config/env
val userId = "user_12345"

val claims = Claims()
claims.setSubject(userId)
claims.setExpiration(System.currentTimeMillis() + 3600000) // 1 hour from now

val token = JwtScala.encode(claims, secret)
// Now send this 'token' back to the client in the response body or a cookie

Where does the logic go to intercept and validate these tokens?

You definitely don't want to manually decode the token inside every single controller method. That's a maintenance nightmare. In Play, the cleanest way to handle this is by creating a custom Action. Think of it as a gatekeeper that runs before your actual logic.

I'll show you a simplified version of a JwtAction. It looks for the Authorization header, strips the "Bearer " prefix, and tries to validate the token. If it fails, we stop right there and return a 401 Unauthorized.

import javax.inject._
import play.api.mvc._
import jwt.JwtScala

class JwtAction @Inject()(val parser: BodyParsers.Default) extends Action {
  def execute(request: Request[A]): Future[Result] = {
    val authHeader = request.headers.get("Authorization")
    
    authHeader match {
      case Some(header) if header.startsWith("Bearer ") =>
        val token = header.substring(7)
        try {
          val claims = JwtScala.decode(token, "your-super-secret-key-keep-it-safe")
          // We've validated the token! Now we pass the user ID along to the controller
          request.flatMap { req => 
            // In a real app, you'd wrap the request in a custom Request type containing the user
            Future.successful(next(req)) 
          }
        } catch {
          case _: Exception => Future.successful(Results.Unauthorized("Invalid or expired token"))
        }
      case _ => 
        Future.successful(Results.Unauthorized("Missing Authorization header"))
    }
  }
}

What happens when a token expires? How do I handle that gracefully?

This is where most developers trip up. When JwtScala.decode hits an expired token, it throws an exception. If you aren't catching that, your whole request crashes. I prefer treating the decoding process as a "failure-prone" operation using a try-catch or wrapping it in an Either.

From the client's perspective, they just get a 401. Your frontend should be smart enough to see that 401 and either redirect the user to the login page or use a "refresh token" to get a new JWT without making the user type their password again. I personally find that keeping the access token short-lived and using a separate, long-lived refresh token stored in an HttpOnly cookie is the gold standard for security.




📋 Practical Task

Implementing a Protected "Post Review" Endpoint for the Book Review API

You are building a Book Review API. Currently, anyone can post a review, which is causing a lot of spam. Your task is to protect the POST /reviews endpoint.

  • Create a custom JwtAction (or an Akka HTTP Directive if you're using Akka) that extracts the userId from the JWT claims.
  • Modify the postReview method in your controller so that it only accepts requests passing through your JwtAction.
  • Ensure that the userId extracted from the token is the one saved as the author of the review in the database, rather than trusting a userId sent in the JSON body of the request.
  • Test the endpoint by sending a request with an expired token and verify that the server returns a 401 Unauthorized instead of a 500 Internal Server Error.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.