From 2671ca2a0a0f1f50eefffab1a86368d387ead2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Cruz=20Lapadula=20Pl=C3=A1?= Date: Wed, 24 May 2023 15:34:29 +0200 Subject: [PATCH] Use subject claim from jwt (#131) Get a wrapped UserRequest containing a user name if there is a sub claim in the JWT token --------- Co-authored-by: Juan Lapadula Co-authored-by: mkr --- .../auth/JWTJsonAuthenticatedAction.scala | 12 +++- build.sbt | 2 +- .../auth/JWTJsonAuthenticatedActionSpec.scala | 68 ++++++++++++++----- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/app/controllers/auth/JWTJsonAuthenticatedAction.scala b/app/controllers/auth/JWTJsonAuthenticatedAction.scala index 848519c2..b3d8744d 100644 --- a/app/controllers/auth/JWTJsonAuthenticatedAction.scala +++ b/app/controllers/auth/JWTJsonAuthenticatedAction.scala @@ -1,5 +1,6 @@ package controllers.auth +import com.google.inject.Inject import com.jayway.jsonpath.JsonPath import net.minidev.json.JSONArray import pdi.jwt.{JwtAlgorithm, JwtClaim, JwtJson} @@ -9,7 +10,7 @@ import play.api.{Configuration, Logging} import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} -class JWTJsonAuthenticatedAction(parser: BodyParsers.Default, appConfig: Configuration)(implicit ec: ExecutionContext) +class JWTJsonAuthenticatedAction @Inject()(parser: BodyParsers.Default, appConfig: Configuration)(implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) with Logging { logger.debug("In JWTJsonAuthenticatedAction") @@ -66,6 +67,13 @@ class JWTJsonAuthenticatedAction(parser: BodyParsers.Default, appConfig: Configu } else true } + private def getUserRequestIfAvailable[A](token: JwtClaim, request: Request[A]): Request[A] = { + token.subject match { + case Some(subject) => UserRequest(subject, request) + case None => request + } + } + private def redirectToLoginPage(): Future[Result] = { Future { Results.Redirect(JWT_LOGIN_URL) @@ -79,7 +87,7 @@ class JWTJsonAuthenticatedAction(parser: BodyParsers.Default, appConfig: Configu getJwtCookie(request) match { case Some(cookie) => isAuthenticated(cookie.value) match { - case Some(token) if isAuthorized(token.content) => block(request) + case Some(token) if isAuthorized(token.content) => block(getUserRequestIfAvailable(token, request)) case _ => redirectToLoginPage() } case None => redirectToLoginPage() diff --git a/build.sbt b/build.sbt index b97c9e4f..88662266 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ import com.typesafe.sbt.GitBranchPrompt name := "search-management-ui" -version := "3.16.0" +version := "3.17.0" scalaVersion := "2.12.17" diff --git a/test/auth/JWTJsonAuthenticatedActionSpec.scala b/test/auth/JWTJsonAuthenticatedActionSpec.scala index 20ab8b45..e009878e 100644 --- a/test/auth/JWTJsonAuthenticatedActionSpec.scala +++ b/test/auth/JWTJsonAuthenticatedActionSpec.scala @@ -1,22 +1,22 @@ package auth -import java.security.{KeyPairGenerator, SecureRandom} -import java.util.Base64 - +import controllers.auth.{JWTJsonAuthenticatedAction, UserRequest} import org.scalatest.concurrent.ScalaFutures import org.scalatest.mockito.MockitoSugar import org.scalatestplus.play.PlaySpec import org.scalatestplus.play.guice.GuiceOneAppPerTest import pdi.jwt.{JwtAlgorithm, JwtJson} import play.api.db.{Database, Databases} -import play.api.{Application, Mode} import play.api.inject.guice.GuiceApplicationBuilder -import play.api.libs.json.Json -import play.api.mvc.{Cookie, Result} +import play.api.libs.json.{JsString, Json} +import play.api.mvc.{Cookie, Request, Result, Results} import play.api.test.FakeRequest import play.api.test.Helpers._ +import play.api.{Application, Mode} -import scala.concurrent.Future +import java.security.{KeyPairGenerator, SecureRandom} +import java.util.Base64 +import scala.concurrent.{ExecutionContext, Future} class JWTJsonAuthenticatedActionSpec extends PlaySpec with MockitoSugar with GuiceOneAppPerTest with ScalaFutures { @@ -58,7 +58,7 @@ class JWTJsonAuthenticatedActionSpec extends PlaySpec with MockitoSugar with Gui "redirect if an invalid jwt token is provided" in { val request = FakeRequest(GET, "/") - .withCookies(buildJWTCookie("test_user", Seq("admin"), Some("invalid_token"))) + .withCookies(buildJWTCookie(Seq("admin"), value = Some("invalid_token"))) val home: Future[Result] = route(app, request).get @@ -70,7 +70,7 @@ class JWTJsonAuthenticatedActionSpec extends PlaySpec with MockitoSugar with Gui "redirect if the user has not the right permissions" in { val request = FakeRequest(GET, "/") - .withCookies(buildJWTCookie("test_user", Seq("not_admin"))) + .withCookies(buildJWTCookie(Seq("not_admin"))) val home: Future[Result] = route(app, request).get @@ -82,7 +82,7 @@ class JWTJsonAuthenticatedActionSpec extends PlaySpec with MockitoSugar with Gui "lead user to SMUI if a valid rsa encoded token is provided" in { val request = FakeRequest(GET, "/") - .withCookies(buildJWTCookie("test_user", Seq("search-manager"))) + .withCookies(buildJWTCookie(Seq("search-manager"))) val home: Future[Result] = route(app, request).get @@ -94,7 +94,7 @@ class JWTJsonAuthenticatedActionSpec extends PlaySpec with MockitoSugar with Gui "let users pass to SMUI if they have the right role even if they also have other roles" in { val request = FakeRequest(GET, "/") - .withCookies(buildJWTCookie("test_user", Seq("search-manager", "barkeeper"))) + .withCookies(buildJWTCookie(Seq("search-manager", "barkeeper"))) val home: Future[Result] = route(app, request).get @@ -105,7 +105,7 @@ class JWTJsonAuthenticatedActionSpec extends PlaySpec with MockitoSugar with Gui "let users pass to SMUI if they have role containing a whitespace character" in { val request = FakeRequest(GET, "/") - .withCookies(buildJWTCookie("test_user", Seq("smui rules analyst"))) + .withCookies(buildJWTCookie(Seq("smui rules analyst"))) val home: Future[Result] = route(app, request).get @@ -116,7 +116,7 @@ class JWTJsonAuthenticatedActionSpec extends PlaySpec with MockitoSugar with Gui "should secure API routes" in { var request = FakeRequest(GET, "/api/v1/inputTags") - .withCookies(buildJWTCookie("test_user", Seq("search-manager"))) + .withCookies(buildJWTCookie(Seq("search-manager"))) var home: Future[Result] = route(app, request).get @@ -135,7 +135,7 @@ class JWTJsonAuthenticatedActionSpec extends PlaySpec with MockitoSugar with Gui "respond correct to api call" in { val request = FakeRequest(GET, "/api/v1/allRulesTxtFiles") - .withCookies(buildJWTCookie("test_user", Seq("search-manager"))) + .withCookies(buildJWTCookie(Seq("search-manager"))) val home: Future[Result] = route(app, request).get @@ -143,6 +143,39 @@ class JWTJsonAuthenticatedActionSpec extends PlaySpec with MockitoSugar with Gui result.header.status mustBe 200 } } + + "return a UserRequest if the subject claim is present" in { + val request = FakeRequest(GET, "/api/v1/allRulesTxtFiles") + .withCookies(buildJWTCookie(Seq("search-manager"))) + val authenticator = app.injector.instanceOf[JWTJsonAuthenticatedAction] + var modifiedRequest: Request[Any] = request + + val authenticated = authenticator.invokeBlock(request, (receivedRequest: Request[Any]) => { + modifiedRequest = receivedRequest + Future.successful(Results.Ok) + }) + + whenReady(authenticated) { _ => + modifiedRequest mustBe a[UserRequest[Any]] + } + } + + "not touch the request if the subject claim is not present" in { + val request = FakeRequest(GET, "/api/v1/allRulesTxtFiles") + .withCookies(buildJWTCookie(Seq("search-manager"), optUserName = None)) + val authenticator = app.injector.instanceOf[JWTJsonAuthenticatedAction] + var modifiedRequest: Request[Any] = request + + val authenticated = authenticator.invokeBlock(request, (receivedRequest: Request[Any]) => { + modifiedRequest = receivedRequest + Future.apply(Results.Ok)(ExecutionContext.global) + }) + + whenReady(authenticated) { _ => + modifiedRequest mustBe request + modifiedRequest must not be a[UserRequest[Any]] + } + } } private def generateRsaKeyPair() = { @@ -151,8 +184,11 @@ class JWTJsonAuthenticatedActionSpec extends PlaySpec with MockitoSugar with Gui keyGen.generateKeyPair() } - private def buildJWTCookie(userName: String, roles: Seq[String], value: Option[String] = None): Cookie = { - val token = Json.obj(("roles", roles), ("user", userName)) + private def buildJWTCookie(roles: Seq[String] = Seq.empty, optUserName: Option[String] = Option("test_user"), value: Option[String] = None) = { + var token = Json.obj(("roles", roles)) + for (userName <- optUserName) { + token = token + ("sub", JsString(userName)) + } Cookie( name = getJwtConfiguration("cookie.name"),