feat: BAC-39 Authentication #114
Submodule knockoutwhistfrontend updated: 6b8488e7a4...240be41dc7
@@ -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")
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,7 +75,9 @@ 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")) {
|
||||||
|
// Use OpenID Connect AuthenticationRequest for OpenID providers
|
||||||
|
new AuthenticationRequest.Builder(
|
||||||
new ResponseType(ResponseType.Value.CODE),
|
new ResponseType(ResponseType.Value.CODE),
|
||||||
new com.nimbusds.oauth2.sdk.Scope(provider.scopes.mkString(" ")),
|
new com.nimbusds.oauth2.sdk.Scope(provider.scopes.mkString(" ")),
|
||||||
new com.nimbusds.oauth2.sdk.id.ClientID(provider.clientId),
|
new com.nimbusds.oauth2.sdk.id.ClientID(provider.clientId),
|
||||||
@@ -84,6 +87,18 @@ class OpenIDConnectService@Inject(ws: WSClient, config: Configuration)(implicit
|
|||||||
.nonce(new Nonce(nonce))
|
.nonce(new Nonce(nonce))
|
||||||
.endpointURI(URI.create(provider.authorizationEndpoint))
|
.endpointURI(URI.create(provider.authorizationEndpoint))
|
||||||
.build()
|
.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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user