feat: BAC-39 Authentication #114

Merged
Janis merged 4 commits from feat/auth into main 2026-01-20 12:28:00 +01:00
110 changed files with 842 additions and 4074 deletions
Showing only changes of commit f8c979ab3d - Show all commits

View File

@@ -1,7 +1,5 @@
package controllers package controllers
import auth.AuthAction
import com.typesafe.config.Config
import logic.user.{SessionManager, UserManager} import logic.user.{SessionManager, UserManager}
import model.users.User import model.users.User
import play.api.Configuration import play.api.Configuration
@@ -22,7 +20,7 @@ class OpenIDController @Inject()(
val config: Configuration val config: Configuration
)(implicit ec: ExecutionContext) extends BaseController { )(implicit ec: ExecutionContext) extends BaseController {
def loginWithProvider(provider: String) = Action.async { implicit request => def loginWithProvider(provider: String): Action[AnyContent] = Action.async { implicit request =>
val state = openIDService.generateState() val state = openIDService.generateState()
val nonce = openIDService.generateNonce() val nonce = openIDService.generateNonce()
@@ -40,7 +38,7 @@ class OpenIDController @Inject()(
} }
} }
def callback(provider: String) = Action.async { implicit request => def callback(provider: String): Action[AnyContent] = Action.async { implicit request =>
val sessionState = request.session.get("oauth_state") val sessionState = request.session.get("oauth_state")
val sessionNonce = request.session.get("oauth_nonce") val sessionNonce = request.session.get("oauth_nonce")
val sessionProvider = request.session.get("oauth_provider") val sessionProvider = request.session.get("oauth_provider")
@@ -63,7 +61,7 @@ class OpenIDController @Inject()(
openIDService.getUserInfo(provider, tokenResponse.accessToken).map { openIDService.getUserInfo(provider, tokenResponse.accessToken).map {
case Some(userInfo) => case Some(userInfo) =>
// Store user info in session for username selection // Store user info in session for username selection
Redirect(config.get[String]("app.url") + "/select-username") Redirect(config.get[String]("openid.selectUserRoute"))
.withSession( .withSession(
"oauth_user_info" -> Json.toJson(userInfo).toString(), "oauth_user_info" -> Json.toJson(userInfo).toString(),
"oauth_provider" -> provider, "oauth_provider" -> provider,
@@ -81,7 +79,7 @@ class OpenIDController @Inject()(
} }
} }
def selectUsername() = Action.async { implicit request => def selectUsername(): Action[AnyContent] = Action.async { implicit request =>
request.session.get("oauth_user_info") match { request.session.get("oauth_user_info") match {
case Some(userInfoJson) => case Some(userInfoJson) =>
val userInfo = Json.parse(userInfoJson).as[OpenIDUserInfo] val userInfo = Json.parse(userInfoJson).as[OpenIDUserInfo]
@@ -90,14 +88,15 @@ class OpenIDController @Inject()(
"email" -> userInfo.email, "email" -> userInfo.email,
"name" -> userInfo.name, "name" -> userInfo.name,
"picture" -> userInfo.picture, "picture" -> userInfo.picture,
"provider" -> userInfo.provider "provider" -> userInfo.provider,
"providerName" -> userInfo.providerName
))) )))
case None => case None =>
Future.successful(Redirect("/login").flashing("error" -> "No authentication information found")) Future.successful(Redirect("/login").flashing("error" -> "No authentication information found"))
} }
} }
def submitUsername() = Action.async { implicit request => def submitUsername(): Action[AnyContent] = Action.async { implicit request =>
val username = request.body.asJson.flatMap(json => (json \ "username").asOpt[String]) val username = request.body.asJson.flatMap(json => (json \ "username").asOpt[String])
.orElse(request.body.asFormUrlEncoded.flatMap(_.get("username").flatMap(_.headOption))) .orElse(request.body.asFormUrlEncoded.flatMap(_.get("username").flatMap(_.headOption)))
val userInfoJson = request.session.get("oauth_user_info") val userInfoJson = request.session.get("oauth_user_info")

View File

@@ -19,6 +19,12 @@ class StubUserManager @Inject()(config: Config) extends UserManager {
name = "Janis", name = "Janis",
passwordHash = UserHash.hashPW("password123") passwordHash = UserHash.hashPW("password123")
), ),
"Leon" -> User(
internalId = 2L,
id = java.util.UUID.randomUUID(),
name = "Jakob",
passwordHash = UserHash.hashPW("password123")
),
"Jakob" -> User( "Jakob" -> User(
internalId = 2L, internalId = 2L,
id = java.util.UUID.fromString("323e4567-e89b-12d3-a456-426614174000"), id = java.util.UUID.fromString("323e4567-e89b-12d3-a456-426614174000"),

View File

@@ -26,8 +26,11 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
else canInteract = Some(InteractionType.Card) else canInteract = Some(InteractionType.Card)
case _ => case _ =>
} }
lock.lock()
websocketActor.foreach(_.solveRequests()) websocketActor.foreach(_.solveRequests())
websocketActor.foreach(_.transmitEventToClient(event)) websocketActor.foreach(_.transmitEventToClient(event))
lock.unlock()
} }
override def id: UUID = user.id override def id: UUID = user.id

View File

@@ -19,7 +19,8 @@ case class OpenIDUserInfo(
email: Option[String], email: Option[String],
name: Option[String], name: Option[String],
picture: Option[String], picture: Option[String],
provider: String provider: String,
providerName: String
) )
object OpenIDUserInfo { object OpenIDUserInfo {
@@ -51,7 +52,7 @@ class OpenIDConnectService@Inject(ws: WSClient, config: Configuration)(implicit
private val providers = Map( private val providers = Map(
"discord" -> OpenIDProvider( "discord" -> OpenIDProvider(
name = "discord", name = "Discord",
clientId = config.get[String]("openid.discord.clientId"), clientId = config.get[String]("openid.discord.clientId"),
clientSecret = config.get[String]("openid.discord.clientSecret"), clientSecret = config.get[String]("openid.discord.clientSecret"),
redirectUri = config.get[String]("openid.discord.redirectUri"), redirectUri = config.get[String]("openid.discord.redirectUri"),
@@ -61,7 +62,7 @@ class OpenIDConnectService@Inject(ws: WSClient, config: Configuration)(implicit
scopes = Set("identify", "email") scopes = Set("identify", "email")
), ),
"keycloak" -> OpenIDProvider( "keycloak" -> OpenIDProvider(
name = "keycloak", name = "Identity",
clientId = config.get[String]("openid.keycloak.clientId"), clientId = config.get[String]("openid.keycloak.clientId"),
clientSecret = config.get[String]("openid.keycloak.clientSecret"), clientSecret = config.get[String]("openid.keycloak.clientSecret"),
redirectUri = config.get[String]("openid.keycloak.redirectUri"), redirectUri = config.get[String]("openid.keycloak.redirectUri"),
@@ -74,16 +75,30 @@ class OpenIDConnectService@Inject(ws: WSClient, config: Configuration)(implicit
def getAuthorizationUrl(providerName: String, state: String, nonce: String): Option[String] = { def getAuthorizationUrl(providerName: String, state: String, nonce: String): Option[String] = {
providers.get(providerName).map { provider => providers.get(providerName).map { provider =>
val authRequest = new AuthenticationRequest.Builder( val authRequest = if (provider.scopes.contains("openid")) {
new ResponseType(ResponseType.Value.CODE), // Use OpenID Connect AuthenticationRequest for OpenID providers
new com.nimbusds.oauth2.sdk.Scope(provider.scopes.mkString(" ")), new AuthenticationRequest.Builder(
new com.nimbusds.oauth2.sdk.id.ClientID(provider.clientId), new ResponseType(ResponseType.Value.CODE),
URI.create(provider.redirectUri) new com.nimbusds.oauth2.sdk.Scope(provider.scopes.mkString(" ")),
) new com.nimbusds.oauth2.sdk.id.ClientID(provider.clientId),
.state(new com.nimbusds.oauth2.sdk.id.State(state)) URI.create(provider.redirectUri)
.nonce(new Nonce(nonce)) )
.endpointURI(URI.create(provider.authorizationEndpoint)) .state(new com.nimbusds.oauth2.sdk.id.State(state))
.build() .nonce(new Nonce(nonce))
.endpointURI(URI.create(provider.authorizationEndpoint))
.build()
} else {
// Use standard OAuth2 AuthorizationRequest for non-OpenID providers (like Discord)
new AuthorizationRequest.Builder(
new ResponseType(ResponseType.Value.CODE),
new com.nimbusds.oauth2.sdk.id.ClientID(provider.clientId)
)
.scope(new com.nimbusds.oauth2.sdk.Scope(provider.scopes.mkString(" ")))
.state(new com.nimbusds.oauth2.sdk.id.State(state))
.redirectionURI(URI.create(provider.redirectUri))
.endpointURI(URI.create(provider.authorizationEndpoint))
.build()
}
authRequest.toURI.toString authRequest.toURI.toString
} }
@@ -139,7 +154,8 @@ class OpenIDConnectService@Inject(ws: WSClient, config: Configuration)(implicit
email = (json \ "email").asOpt[String], email = (json \ "email").asOpt[String],
name = (json \ "name").asOpt[String].orElse((json \ "login").asOpt[String]), name = (json \ "name").asOpt[String].orElse((json \ "login").asOpt[String]),
picture = (json \ "picture").asOpt[String].orElse((json \ "avatar_url").asOpt[String]), picture = (json \ "picture").asOpt[String].orElse((json \ "avatar_url").asOpt[String]),
provider = providerName provider = providerName,
providerName = provider.name
)) ))
} else { } else {
None None

View File

@@ -25,11 +25,13 @@ play.filters.cors {
# Local Development OpenID Connect Configuration # Local Development OpenID Connect Configuration
openid { openid {
selectUserRoute="http://localhost:5173/select-username"
discord { discord {
clientId = ${?DISCORD_CLIENT_ID} clientId = ${?DISCORD_CLIENT_ID}
clientId = "your-discord-client-id" clientId = "1462555597118509126"
clientSecret = ${?DISCORD_CLIENT_SECRET} clientSecret = ${?DISCORD_CLIENT_SECRET}
clientSecret = "your-discord-client-secret" clientSecret = "xZZrdd7_tNpfJgnk-6phSG53DSTy-eMK"
redirectUri = ${?DISCORD_REDIRECT_URI} redirectUri = ${?DISCORD_REDIRECT_URI}
redirectUri = "http://localhost:9000/auth/discord/callback" redirectUri = "http://localhost:9000/auth/discord/callback"
} }

View File

@@ -17,7 +17,7 @@ play.filters.cors {
# OpenID Connect Configuration # OpenID Connect Configuration
openid { openid {
selectUserRoute="https://knockout.janis-eccarius.de/select-user" selectUserRoute="https://knockout.janis-eccarius.de/select-username"
discord { discord {
clientId = ${?DISCORD_CLIENT_ID} clientId = ${?DISCORD_CLIENT_ID}

View File

@@ -11,3 +11,23 @@ play.filters.cors {
allowedHttpMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"] allowedHttpMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"] allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"]
} }
openid {
selectUserRoute="https://st.knockout.janis-eccarius.de/select-username"
discord {
clientId = ${?DISCORD_CLIENT_ID}
clientSecret = ${?DISCORD_CLIENT_SECRET}
redirectUri = ${?DISCORD_REDIRECT_URI}
redirectUri = "https://st.knockout.janis-eccarius.de/auth/discord/callback"
}
keycloak {
clientId = "your-keycloak-client-id"
clientSecret = "your-keycloak-client-secret"
redirectUri = "https://st.knockout.janis-eccarius.de/api/auth/keycloak/callback"
authUrl = ${?KEYCLOAK_AUTH_URL}
authUrl = "https://identity.janis-eccarius.de/realms/master"
}
}