feat(user-sessions): add JWT-based session management and main menu route

This commit is contained in:
2025-10-29 10:31:49 +01:00
parent 93b0766138
commit 98a3f6319f
9 changed files with 138 additions and 6 deletions

View File

@@ -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
}

View File

@@ -0,0 +1,9 @@
{
"version": "1",
"name": "KnockOutWhist",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@@ -0,0 +1,3 @@
vars {
host: http://localhost:9000
}

View File

@@ -39,7 +39,7 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
commonSettings, commonSettings,
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test, libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test,
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12", 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" libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.2"
) )

View File

@@ -24,13 +24,31 @@ import javax.inject.*
@Singleton @Singleton
class UserController @Inject()(val controllerComponents: ControllerComponents, val sessionManager: SessionManager, val userManager: UserManager) extends BaseController { 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] = { def login(): Action[AnyContent] = {
Action { implicit request => Action { implicit request =>
val session = request.cookies.get("sessionId") val session = request.cookies.get("sessionId")
if (session.isDefined) { if (session.isDefined) {
val possibleUser = sessionManager.getUserBySession(session.get.value) val possibleUser = sessionManager.getUserBySession(session.get.value)
if (possibleUser.isDefined) { if (possibleUser.isDefined) {
Redirect("/main-menu") Redirect("/mainmenu")
} else } else
{ {
Ok(views.html.login()) Ok(views.html.login())
@@ -54,6 +72,7 @@ class UserController @Inject()(val controllerComponents: ControllerComponents, v
Cookie("sessionId", sessionManager.createSession(possibleUser.get)) Cookie("sessionId", sessionManager.createSession(possibleUser.get))
) )
} else { } else {
println("Failed login attempt for user: " + username)
Unauthorized("Invalid username or password") Unauthorized("Invalid username or password")
} }
} else { } else {

View File

@@ -1,18 +1,27 @@
package logic.user.impl package logic.user.impl
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.typesafe.config.Config import com.typesafe.config.Config
import logic.user.SessionManager import logic.user.SessionManager
import model.users.User import model.users.User
import services.JwtKeyProvider
import javax.inject.{Inject, Singleton} import javax.inject.{Inject, Singleton}
@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 = { override def createSession(user: User): String = {
//TODO create JWT token instead of random string
//Write session identifier to cache and DB //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 sessionId
} }

View File

@@ -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.")
}
}
}

View File

@@ -1 +1,12 @@
# https://www.playframework.com/documentation/latest/Configuration # 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}
}

View File

@@ -13,7 +13,7 @@ GET /assets/*file controllers.Assets.versioned(path="/public",
GET /rules controllers.HomeController.rules() GET /rules controllers.HomeController.rules()
GET /mainmenu controllers.UserController.mainMenu()
GET /login controllers.UserController.login() GET /login controllers.UserController.login()
POST /login controllers.UserController.login_Post() POST /login controllers.UserController.login_Post()
GET /logout controllers.UserController.logout() GET /logout controllers.UserController.logout()