DEV Community

Cover image for Cookie Authentication with Scalatra and JWTs
Jennifer Konikowski
Jennifer Konikowski

Posted on

3 1

Cookie Authentication with Scalatra and JWTs

Originally posted on jmkoni.com

As part of my work with Arcadia, I've built a Rails application that added a cookie that contains a JWT (pronounced jot). Great! That was fairly simple. Then I had to go over to our Scala application and get it to accept the JWT as identification. Right now, we were keeping it pretty simple and we only care if it's valid. This post will cover what I think is the simplest way to do that, from start to finish. Or you can skip all that and just go look at the full gist.

We want to start off with the JWT parsing. And before we add the code to actually do that, let's add some tests! I decided to use the JWT Scala library and, in particular, jwt-core. It had, in my opinion, the most easy-to-understand documentation so I could indeed RTFM and get my work done. Since I didn't need to add encoding in the actual application (the tokens would be encoded in another application), I added a quick line to encode a token within the tests.

import org.scalatest.{Matchers, WordSpec}
import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim}
class JwtTests extends WordSpec with Matchers {
val expirationNumber = 10
"A user data token" can {
"valid token" should {
"be decoded and return object" in {
val validToken = Jwt.encode(JwtClaim({
"""{"data": {"email":"heather@example.com"}, "iss": "localhost"}"""
}).issuedNow.expiresIn(expirationNumber), Configuration.SecretKey, JwtAlgorithm.HS256)
val decodedToken = JWT.parseUserJwt(validToken)
assert(decodedToken.isSuccess == true)
assert(decodedToken.get.email == "heather@koni.com")
}
}
"invalid token" should {
"not decode" in {
val invalidToken = Jwt.encode(JwtClaim({
"""{"hi": true}"""
}).issuedNow.expiresIn(expirationNumber), Configuration.SecretKey, JwtAlgorithm.HS256)
val decodedToken = JWT.parseUserJwt(invalidToken)
assert(decodedToken.isSuccess == false)
}
}
}
}
view raw JwtTests.scala hosted with ❤ by GitHub

Now that I have my tests, let's add the actual code to decode the JWT! Thanks to JWT Scala, this is pretty simple! The real secret sauce is in this line: userTokenData = parse(decoded).extract[Token].data. That does a lot of heavy lifting! decoded is just a string and parse turns it into this Jvalue object thanks to json4s, but that object is a bit hard to work with. However, I can extract it out to my case class, Token, which is downright magical. If it doesn't include all the fields that I have in Token, it will produce an error. Perfect!

import org.json4s.DefaultFormats
import org.json4s.jackson.JsonMethods.parse
import pdi.jwt.{Jwt, JwtAlgorithm}
import scala.util.Try
/*
* This object decodes JWTs that are created by another application.
* There are a few different options available for parsing JWTs and I went with:
* http://pauldijou.fr/jwt-scala/samples/jwt-core/
*/
object JWT {
implicit val formats = DefaultFormats
def parseUserJwt(token: String): Try[UserTokenData] = {
for {
decoded <- Jwt.decode(token,
Configuration.SecretKey,
Seq(JwtAlgorithm.HS256)) // this will return a string
userTokenData = parse(decoded).extract[Token].data // this parses the string to JSON and extracts to a token
} yield userTokenData
}
}
case class Token(data: UserTokenData, exp: Int, iat: Int, iss: String)
case class UserTokenData(email: String)
view raw Jwt.scala hosted with ❤ by GitHub

Next I need a reusable Authentication object. This wasn't too bad because I found out that HttpServletRequest has a method called getCookies which... returns the cookies. Excellent. I'm sure this looks weird as an Either, but in this case I really did want Some or None because I didn't care about returning the error to the actual user. I did want to log it though, hence the liberal use of println.

import javax.servlet.http.{Cookie, HttpServletRequest}
import play.twirl.api.{Html, HtmlFormat}
import scala.util.{Failure, Success}
object Authentication {
def authenticateCookie(
request: HttpServletRequest): Option[UserTokenData] = {
val token =
request.getCookies.find((c: Cookie) => c.getName == "application_cookie")
if (token.isEmpty) {
val authFailure = AuthenticationFailure(request.getHeader("User-Agent"),
request.getRequestURL.toString,
request.getRemoteAddr)
println("Error: application_cookie cookie not found")
println("More information:")
println(authFailure.toString)
return None
}
val userToken = JWT.parseUserJwt(token.get.getValue)
userToken match {
case Success(utd) => Some(utd)
case Failure(t) => {
println("Error while parsing application_cookie cookie: " + t.toString)
None
}
}
}
}
case class AuthenticationFailure(userAgent: String,
url: String,
remoteAddr: String) {
override def toString = {
"AuthenticationFailure(\n" +
" User-Agent: " + userAgent + "\n" +
" Request URL: " + url + "\n" +
" Remote Address: " + remoteAddr + "\n" +
")"
}
}

Last, but definitely not least, I need a servlet. Well... tests for the servlet, then the servlet 😛. This is where I actually ran into trouble because I wasn't sure how to pass cookies to the get request in a test. With some help from my boss, we found out that get takes a headers param and you can pass a cookie if it looks like this: headers = Map("Cookie" -> cookie_value). To be honest, it required a bit of trial and error and I'm still trying to figure out exactly what values are being passed.

Whhhyyyy Scala!!!!

import org.eclipse.jetty.http.HttpStatus
import org.scalatra.test.scalatest.ScalatraFunSuite
import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim}
import java.net.HttpCookie
class MyServletTests extends ScalatraFunSuite {
addServlet(classOf[MyServlet], "/*")
test("GET / on MyServlet should return status 200 with invalid token"){
get("/", params = Map.empty, headers = cookieHeaderWith(Map("testcookie"->"what"))) {
status should equal (HttpStatus.OK_200)
body should include ("Cookie is invalid.")
}
}
test("GET / on MyServlet should return status 200 with valid token"){
val expirationNumber = 10
val validToken = Jwt.encode(JwtClaim({
"""{"data": {"email":"heather@example.com"}, "iss": "localhost"}"""
}).issuedNow.expiresIn(expirationNumber), common.Configuration.SecretKey, JwtAlgorithm.HS256)
get("/", params = Map.empty, headers = cookieHeaderWith(Map("application_cookie"-> validToken))) {
status should equal (HttpStatus.OK_200)
body should include ("Welcome to my site!")
}
}
/**
* Helper to create a headers map with the cookies specified. Merge with another map for more headers.
*
* This allows only basic cookies, no expiry or domain set.
*
* @param cookies key-value pairs
* @return a map suitable for passing to a get() or post() Scalatra test method
*/
def cookieHeaderWith(cookies: Map[String, String]): Map[String, String] = {
val asHttpCookies = cookies.map { case (k, v) => new HttpCookie(k, v) }
val headerValue = asHttpCookies.mkString("; ")
Map("Cookie" -> headerValue)
}
}

And finally... my servlet! Short and sweet.

import org.scalatra.ScalatraServlet
class MyServlet extends ScalatraServlet {
get("/") {
authenticateCookie(request) match {
case Some(_) => {
views.html.hello()
}
case None => {
views.html.error("Cookie is invalid.")
}
}
}
}
view raw MyServlet.scala hosted with ❤ by GitHub

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

Top comments (3)

Collapse
 
colinmtech profile image
Colin Morgan

pronounced jot

Woah, woah, woah. Let me stop you right there. I feel like we should take a vote on this one.

Collapse
 
jennifer profile image
Jennifer Konikowski

I didn't make this up? You can google "jot jwt" or just look at Wikipedia: en.wikipedia.org/wiki/JSON_Web_Token

Collapse
 
colinmtech profile image
Colin Morgan

I'm just messing with you. Though I've never heard it called that before. I don't even have a viable alternative to suggest.

Thanks for the article =)

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay