Last active
June 19, 2019 10:39
Star
You must be signed in to star a gist
Cookie Authentication with Scalatra and JWTs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" + | |
")" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.") | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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