From 81d0d66d14476ea9ecf40bc3ff9ff50fc9b8b35e Mon Sep 17 00:00:00 2001
From: Felipe D'Abrantes <felidabrantes@gmail>
Date: Tue, 11 Apr 2023 00:48:26 +0100
Subject: [PATCH] Create authentication middleware

---
 .../actions/AuthenticatedUserAction.scala     | 29 ++++++++
 .../models/actions/AuthenticationFilter.scala | 29 ++++++++
 .../actions/AuthenticationRequest.scala       | 66 +++++++++++++++++++
 backend-services/feed-service/build.sbt       |  1 +
 .../feed-service/conf/application.conf        |  3 +
 5 files changed, 128 insertions(+)
 create mode 100644 backend-services/feed-service/app/models/actions/AuthenticatedUserAction.scala
 create mode 100644 backend-services/feed-service/app/models/actions/AuthenticationFilter.scala
 create mode 100644 backend-services/feed-service/app/models/actions/AuthenticationRequest.scala

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 00000000..7382920c
--- /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 00000000..3e8ed31a
--- /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 00000000..cf6333b7
--- /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 e42fe9c6..8ac4696d 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 21793f26..cd190de6 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 = ""
-- 
GitLab