diff --git a/backend-services/feed-service/app/models/actions/AuthenticatedUserAction.scala b/backend-services/feed-service/app/models/actions/AuthenticatedUserAction.scala new file mode 100644 index 0000000000000000000000000000000000000000..7382920cd76e82c6c012bd38434bde90347ae552 --- /dev/null +++ b/backend-services/feed-service/app/models/actions/AuthenticatedUserAction.scala @@ -0,0 +1,29 @@ +package models.actions + +import models.actions.AuthenticationRequest +import play.api.mvc.{ActionBuilder, BodyParsers, Request, Result, AnyContent} + +import scala.concurrent.Future + +import javax.inject.Inject +import scala.concurrent.ExecutionContext + + +/** + * The authentication action builder that combines the request transformation and filtering. + */ +class AuthenticatedUserAction @Inject()(authenticationTransformer: AuthenticationTransformer, authenticationFilter: AuthenticationFilter) + (implicit val parser: BodyParsers.Default, val executionContext: ExecutionContext) + extends ActionBuilder[AuthenticationRequest, AnyContent] { + + /** + * Invoke the main controller block, with the transformations and filtering middleware. + * + * @param request The incoming request. + * @param block The block of code to invoke. + * @return A future of the result. + */ + override def invokeBlock[A](request: Request[A], block: AuthenticationRequest[A] => Future[Result]): Future[Result] = { + (authenticationTransformer andThen authenticationFilter).invokeBlock(request, block) + } +} diff --git a/backend-services/feed-service/app/models/actions/AuthenticationFilter.scala b/backend-services/feed-service/app/models/actions/AuthenticationFilter.scala new file mode 100644 index 0000000000000000000000000000000000000000..3e8ed31a8ec563c9ef2f0289a256368eb48fa8e2 --- /dev/null +++ b/backend-services/feed-service/app/models/actions/AuthenticationFilter.scala @@ -0,0 +1,29 @@ +package models.actions + +import models.actions.AuthenticationRequest +import play.api.mvc.{ActionFilter, Result, Results} + +import scala.concurrent.{Future, ExecutionContext} + +import javax.inject.Inject + + +/** + * The authentication action filter that verifies the request contains a user ID. + */ +class AuthenticationFilter @Inject() (implicit val executionContext: ExecutionContext) extends ActionFilter[AuthenticationRequest] { + + /** + * Determines whether to process a request. + * Decides whether to immediately intercept the request or continue processing the request. + * + * @param request The incoming request. + * @return An optional Forbidden Result with which to abort the request. + */ + override def filter[A](request: AuthenticationRequest[A]): Future[Option[Result]] = Future.successful { + if (!request.userId.isDefined) + Some(Results.Forbidden("Invalid JWT Token")) + else + None + } +} diff --git a/backend-services/feed-service/app/models/actions/AuthenticationRequest.scala b/backend-services/feed-service/app/models/actions/AuthenticationRequest.scala new file mode 100644 index 0000000000000000000000000000000000000000..cf6333b7dadd6712dee50e228daf1238f175e1fa --- /dev/null +++ b/backend-services/feed-service/app/models/actions/AuthenticationRequest.scala @@ -0,0 +1,66 @@ +package models.actions + +import com.typesafe.config.ConfigFactory + +import play.api.mvc.{ActionTransformer, Request, WrappedRequest} + +import scala.concurrent.{Future, ExecutionContext} +import scala.util.Try +import org.bson.types.ObjectId + +import pdi.jwt.{JwtJson, JwtAlgorithm, JwtClaim} +import play.api.libs.json.Json + +import javax.inject.Inject + + +class AuthenticationRequest[A](val userId: Option[ObjectId], request: Request[A]) extends WrappedRequest[A](request) + + +/** + * The authentication action transformer that transforms the incoming base request to a user request. + */ +class AuthenticationTransformer @Inject() (implicit val executionContext: ExecutionContext) extends ActionTransformer[Request, AuthenticationRequest] { + + /** + * Transforms the existing request from a Request to UserRequest. + * + * @param request The incoming request. + * @return The new parameter to pass to the Action block. + */ + override def transform[A](request: Request[A]) = Future.successful { + println(request) + val userId: Option[ObjectId] = processJWT(request) + new AuthenticationRequest(userId, request) + } + + /** + * Processes the JWT token by decoding and validating it. + * + * @param request The incoming request. + * @return The user ID specified in the JWT's payload. + */ + def processJWT[A](request: Request[A]): Option[ObjectId] = { + val privateKey = ConfigFactory.load().getString("jwt.privateKey") + + try { + val authHeader = request.headers.get("Authorization").get + val token = authHeader.substring(7) + println(s"JWT Token Received: $token") + + val payload: Try[JwtClaim] = JwtJson.decode(token, privateKey, Seq(JwtAlgorithm.HS256)) + + val content = payload.get.content + val jsonContent = Json.parse(content) + val userId = (jsonContent \ "userId").as[String] + + Some(new ObjectId(userId)) + } + catch { + case ex: Throwable => { + println(ex) + None + } + } + } +} diff --git a/backend-services/feed-service/build.sbt b/backend-services/feed-service/build.sbt index e42fe9c65f8aa4ac7c87ce9f18c2b68873419c91..8ac4696d90efd925ef9357082303b144d876854b 100644 --- a/backend-services/feed-service/build.sbt +++ b/backend-services/feed-service/build.sbt @@ -20,3 +20,4 @@ libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0 libraryDependencies += "org.mongodb.scala" %% "mongo-scala-driver" % "4.3.0" libraryDependencies += "com.typesafe.play" %% "play-ws" % "2.8.10" libraryDependencies += "com.typesafe.play" %% "play-ahc-ws-standalone" % "2.1.10" +libraryDependencies += "com.github.jwt-scala" %% "jwt-play-json" % "9.2.0" diff --git a/backend-services/feed-service/conf/application.conf b/backend-services/feed-service/conf/application.conf index 21793f26d6ae0be584bda9b29c3fb4b4589d9d7d..cd190de63d1862ce68daa1d58dbd3993bc1c4e13 100644 --- a/backend-services/feed-service/conf/application.conf +++ b/backend-services/feed-service/conf/application.conf @@ -2,3 +2,6 @@ mongodb.uri="mongodb://localhost:27017/" mongo.feedService.db = "feed-service" mongo.dailies.collection = "dailies" + +# JWT Authenticationn +jwt.privateKey = ""