feat(user-sessions): add JWT-based session management and main menu route
This commit is contained in:
25
bruno/KnockOutWhist/Login.bru
Normal file
25
bruno/KnockOutWhist/Login.bru
Normal 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
|
||||||
|
}
|
||||||
9
bruno/KnockOutWhist/bruno.json
Normal file
9
bruno/KnockOutWhist/bruno.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "KnockOutWhist",
|
||||||
|
"type": "collection",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
".git"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
bruno/KnockOutWhist/environments/Local.bru
Normal file
3
bruno/KnockOutWhist/environments/Local.bru
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
vars {
|
||||||
|
host: http://localhost:9000
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
knockoutwhistweb/app/services/JwtKeyProvider.scala
Normal file
56
knockoutwhistweb/app/services/JwtKeyProvider.scala
Normal 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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user