Skip to content

Instantly share code, notes, and snippets.

@jmkoni
Last active June 19, 2019 10:39
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmkoni/0b1c807f014f8d6f4fc8abe442b0161d to your computer and use it in GitHub Desktop.
Save jmkoni/0b1c807f014f8d6f4fc8abe442b0161d to your computer and use it in GitHub Desktop.
Cookie Authentication with Scalatra and JWTs
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" +
")"
}
}
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)
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)
}
}
}
}
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.")
}
}
}
}
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)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment