From b6f0089d64e2e3fc9419630b35a9c7f7afa50ad5 Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 29 Oct 2025 10:31:49 +0100 Subject: [PATCH] feat(user-sessions): add JWT-based session management and main menu route --- bruno/KnockOutWhist/Login.bru | 25 +++++++++ bruno/KnockOutWhist/bruno.json | 9 +++ bruno/KnockOutWhist/environments/Local.bru | 3 + build.sbt | 2 +- .../app/controllers/UserController.scala | 21 ++++++- .../logic/user/impl/BaseSessionManager.scala | 15 ++++- .../app/services/JwtKeyProvider.scala | 56 +++++++++++++++++++ knockoutwhistweb/conf/application.conf | 11 ++++ knockoutwhistweb/conf/routes | 2 +- 9 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 bruno/KnockOutWhist/Login.bru create mode 100644 bruno/KnockOutWhist/bruno.json create mode 100644 bruno/KnockOutWhist/environments/Local.bru create mode 100644 knockoutwhistweb/app/services/JwtKeyProvider.scala diff --git a/bruno/KnockOutWhist/Login.bru b/bruno/KnockOutWhist/Login.bru new file mode 100644 index 0000000..4c266ed --- /dev/null +++ b/bruno/KnockOutWhist/Login.bru @@ -0,0 +1,25 @@ +meta { + name: Login + type: http + seq: 1 +} + +post { + url: {{host}}/login + body: formUrlEncoded + auth: inherit +} + +body:form-urlencoded { + username: Janis + password: password123 +} + +body:multipart-form { + username: Janis + password: password123 +} + +settings { + encodeUrl: true +} diff --git a/bruno/KnockOutWhist/bruno.json b/bruno/KnockOutWhist/bruno.json new file mode 100644 index 0000000..c687c5e --- /dev/null +++ b/bruno/KnockOutWhist/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "KnockOutWhist", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/bruno/KnockOutWhist/environments/Local.bru b/bruno/KnockOutWhist/environments/Local.bru new file mode 100644 index 0000000..b22b9b6 --- /dev/null +++ b/bruno/KnockOutWhist/environments/Local.bru @@ -0,0 +1,3 @@ +vars { + host: http://localhost:9000 +} diff --git a/build.sbt b/build.sbt index 9485c6f..50d5526 100644 --- a/build.sbt +++ b/build.sbt @@ -39,7 +39,7 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb")) commonSettings, libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test, libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12", -// libraryDependencies += "com.auth0" % "java-jwt" % "4.5.0", + libraryDependencies += "com.auth0" % "java-jwt" % "4.3.0", libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.2" ) diff --git a/knockoutwhistweb/app/controllers/UserController.scala b/knockoutwhistweb/app/controllers/UserController.scala index af4831c..db36946 100644 --- a/knockoutwhistweb/app/controllers/UserController.scala +++ b/knockoutwhistweb/app/controllers/UserController.scala @@ -24,13 +24,31 @@ import javax.inject.* @Singleton class UserController @Inject()(val controllerComponents: ControllerComponents, val sessionManager: SessionManager, val userManager: UserManager) extends BaseController { + def mainMenu() : Action[AnyContent] = { + Action { implicit request => + val session = request.cookies.get("sessionId") + if (session.isDefined) { + val possibleUser = sessionManager.getUserBySession(session.get.value) + if (possibleUser.isDefined) { + Ok("Main Menu for user: " + possibleUser.get.name) + } else + { + println("Invalid session, redirecting to login") + Redirect("/login") + } + } else { + Redirect("/login") + } + } + } + def login(): Action[AnyContent] = { Action { implicit request => val session = request.cookies.get("sessionId") if (session.isDefined) { val possibleUser = sessionManager.getUserBySession(session.get.value) if (possibleUser.isDefined) { - Redirect("/main-menu") + Redirect("/mainmenu") } else { Ok(views.html.login()) @@ -54,6 +72,7 @@ class UserController @Inject()(val controllerComponents: ControllerComponents, v Cookie("sessionId", sessionManager.createSession(possibleUser.get)) ) } else { + println("Failed login attempt for user: " + username) Unauthorized("Invalid username or password") } } else { diff --git a/knockoutwhistweb/app/logic/user/impl/BaseSessionManager.scala b/knockoutwhistweb/app/logic/user/impl/BaseSessionManager.scala index 70d7d9d..b7ba80e 100644 --- a/knockoutwhistweb/app/logic/user/impl/BaseSessionManager.scala +++ b/knockoutwhistweb/app/logic/user/impl/BaseSessionManager.scala @@ -1,18 +1,27 @@ package logic.user.impl +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm import com.typesafe.config.Config import logic.user.SessionManager import model.users.User +import services.JwtKeyProvider import javax.inject.{Inject, Singleton} @Singleton -class BaseSessionManager @Inject()(val config: Config) extends SessionManager { +class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val config: Config) extends SessionManager { + + private val algorithm = Algorithm.RSA512(keyProvider.publicKey, keyProvider.privateKey) override def createSession(user: User): String = { - //TODO create JWT token instead of random string //Write session identifier to cache and DB - val sessionId = java.util.UUID.randomUUID().toString + val sessionId = JWT.create() + .withIssuer(config.getString("auth.issuer")) + .withAudience(config.getString("auth.audience")) + .withSubject(user.internalId.toString) + .sign(algorithm) + //TODO write to DB sessionId } diff --git a/knockoutwhistweb/app/services/JwtKeyProvider.scala b/knockoutwhistweb/app/services/JwtKeyProvider.scala new file mode 100644 index 0000000..9a4d624 --- /dev/null +++ b/knockoutwhistweb/app/services/JwtKeyProvider.scala @@ -0,0 +1,56 @@ +package services + +import java.nio.file.{Files, Paths} +import java.security.{KeyFactory, PrivateKey, PublicKey} +import java.security.spec.X509EncodedKeySpec +import java.util.Base64 +import javax.inject.* +import play.api.Configuration + +import java.security.interfaces.{RSAPrivateKey, RSAPublicKey} + +@Singleton +class JwtKeyProvider @Inject()(config: Configuration) { + + private def cleanPem(pem: String): String = + pem.replaceAll("-----BEGIN (.*)-----", "") + .replaceAll("-----END (.*)-----", "") + .replaceAll("\\s", "") + + private def loadPublicKeyFromPem(pem: String): RSAPublicKey = { + val decoded = Base64.getDecoder.decode(cleanPem(pem)) + val spec = new X509EncodedKeySpec(decoded) + KeyFactory.getInstance("RSA").generatePublic(spec).asInstanceOf[RSAPublicKey] + } + + private def loadPrivateKeyFromPem(pem: String): RSAPrivateKey = { + val decoded = Base64.getDecoder.decode(cleanPem(pem)) + val spec = new X509EncodedKeySpec(decoded) + KeyFactory.getInstance("RSA").generatePrivate(spec).asInstanceOf[RSAPrivateKey] + } + + val publicKey: RSAPublicKey = { + val pemOpt = config.getOptional[String]("auth.publicKeyPem") + val fileOpt = config.getOptional[String]("auth.publicKeyFile") + + pemOpt.orElse(fileOpt.map { path => + new String(Files.readAllBytes(Paths.get(path))) + }) match { + case Some(pem) => loadPublicKeyFromPem(pem) + case None => throw new RuntimeException("No RSA public key configured.") + } + } + + val privateKey: RSAPrivateKey = { + val pemOpt = config.getOptional[String]("auth.privateKeyPem") + val fileOpt = config.getOptional[String]("auth.privateKeyFile") + + pemOpt.orElse(fileOpt.map { path => + new String(Files.readAllBytes(Paths.get(path))) + }) match { + case Some(pem) => loadPrivateKeyFromPem(pem) + case None => throw new RuntimeException("No RSA private key configured.") + } + } + +} diff --git a/knockoutwhistweb/conf/application.conf b/knockoutwhistweb/conf/application.conf index cb94680..bc03440 100644 --- a/knockoutwhistweb/conf/application.conf +++ b/knockoutwhistweb/conf/application.conf @@ -1 +1,12 @@ # https://www.playframework.com/documentation/latest/Configuration +play.filters.disabled += play.filters.csrf.CSRFFilter + + +auth { + issuer = "knockoutwhistweb" + audience = "ui" + privateKeyFile = D:\Workspaces\Gitops\rsa512-private.pem # ${?PUBLIC_KEY_FILE} + privateKeyPem = ${?PUBLIC_KEY_PEM} + publicKeyFile = D:\Workspaces\Gitops\rsa512-public.pem #${?PUBLIC_KEY_FILE} + publicKeyPem = ${?PUBLIC_KEY_PEM} +} diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index 565ed7d..d0b13eb 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -13,7 +13,7 @@ GET /assets/*file controllers.Assets.versioned(path="/public", GET /rules controllers.HomeController.rules() - +GET /mainmenu controllers.UserController.mainMenu() GET /login controllers.UserController.login() POST /login controllers.UserController.login_Post() GET /logout controllers.UserController.logout() \ No newline at end of file