Force SSO user registration due to a bug

This commit is contained in:
GRIMSIM
2025-07-11 11:02:54 +02:00
parent 7ac1377c57
commit a34bd741e3
5 changed files with 60 additions and 32 deletions
@@ -3,8 +3,8 @@ import withConfigPage from "Frontend/components/administration/withConfigPage";
import * as Yup from 'yup'; import * as Yup from 'yup';
import ConfigFormField from "Frontend/components/administration/ConfigFormField"; import ConfigFormField from "Frontend/components/administration/ConfigFormField";
import Section from "Frontend/components/general/Section"; import Section from "Frontend/components/general/Section";
import {addToast, Button} from "@heroui/react"; import {addToast, Button, Checkbox, CheckboxGroup, Tooltip} from "@heroui/react";
import {MagicWand} from "@phosphor-icons/react"; import {MagicWand, Warning} from "@phosphor-icons/react";
function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) { function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
@@ -50,8 +50,21 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
<Section title="SSO user handling"/> <Section title="SSO user handling"/>
<div className="flex flex-row items-baseline"> <div className="flex flex-row items-baseline">
<ConfigFormField configElement={getConfig("sso.oidc.auto-register-new-users")} <CheckboxGroup className="flex flex-col flex-1 items-baseline gap-2"
isDisabled={!formik.values.sso.oidc.enabled}/> value={["auto-register-new-users"]}>
<div className="flex flex-row gap-2">
<Checkbox className="items-baseline" value="auto-register-new-users" isDisabled>
Automatically create new users after registration
</Checkbox>
<Tooltip content={"Currently not configurable due to a bug"} placement="right">
<Warning weight="fill"/>
</Tooltip>
</div>
</CheckboxGroup>
{/*TODO: enable when the issues with unregistered SSO users are sorted
<ConfigFormField configElement={getConfig("sso.oidc.auto-register-new-users")} isDisabled={!formik.values.sso.oidc.enabled}/>
*/}
<ConfigFormField configElement={getConfig("sso.oidc.match-existing-users-by")} <ConfigFormField configElement={getConfig("sso.oidc.match-existing-users-by")}
isDisabled={!formik.values.sso.oidc.enabled || isDisabled={!formik.values.sso.oidc.enabled ||
!formik.values.sso.oidc["auto-register-new-users"]}/> !formik.values.sso.oidc["auto-register-new-users"]}/>
@@ -1,12 +1,12 @@
package org.gameyfin.app.core.security package org.gameyfin.app.core.security
import org.gameyfin.app.config.ConfigService
import org.gameyfin.app.users.UserService
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import org.gameyfin.app.config.ConfigProperties import org.gameyfin.app.config.ConfigProperties
import org.gameyfin.app.config.ConfigService
import org.gameyfin.app.config.MatchUsersBy import org.gameyfin.app.config.MatchUsersBy
import org.gameyfin.app.users.RoleService import org.gameyfin.app.users.RoleService
import org.gameyfin.app.users.UserService
import org.gameyfin.app.users.entities.User import org.gameyfin.app.users.entities.User
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.core.oidc.user.OidcUser import org.springframework.security.oauth2.core.oidc.user.OidcUser
@@ -42,11 +42,13 @@ class SsoAuthenticationSuccessHandler(
// User could not be found in the database // User could not be found in the database
if (matchedUser == null) { if (matchedUser == null) {
// TODO: User registration is currently forced, but this should be configurable.
// However, this causes conflict with user preferences and game entities (since both reference the user entity)
// Check if new user registration is enabled // Check if new user registration is enabled
if (config.get(ConfigProperties.SSO.OIDC.AutoRegisterNewUsers) == false) { //if (config.get(ConfigProperties.SSO.OIDC.AutoRegisterNewUsers) == false) {
response.sendRedirect("/") // response.sendRedirect("/")
return // return
} //
// Register as new user // Register as new user
matchedUser = User(oidcUser) matchedUser = User(oidcUser)
@@ -26,6 +26,7 @@ import org.gameyfin.pluginapi.gamemetadata.*
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -125,8 +126,12 @@ class GameService(
val existingGame = gameRepository.findByIdOrNull(gameUpdateDto.id) val existingGame = gameRepository.findByIdOrNull(gameUpdateDto.id)
?: throw IllegalArgumentException("Game with ID $gameUpdateDto.id not found") ?: throw IllegalArgumentException("Game with ID $gameUpdateDto.id not found")
val userDetails = SecurityContextHolder.getContext().authentication.principal as UserDetails val userDetails = SecurityContextHolder.getContext().authentication.principal
val user = userService.getByUsernameNonNull(userDetails.username) val user = when (userDetails) {
is UserDetails -> userService.getByUsernameNonNull(userDetails.username)
is OidcUser -> userService.getByUsernameNonNull(userDetails.preferredUsername)
else -> throw IllegalStateException("Unkown user type: ${userDetails::class.java.name}")
}
// Update only non-null fields // Update only non-null fields
gameUpdateDto.title?.let { gameUpdateDto.title?.let {
@@ -1,12 +1,12 @@
package org.gameyfin.app.users package org.gameyfin.app.users
import com.vaadin.hilla.Endpoint import com.vaadin.hilla.Endpoint
import org.gameyfin.app.users.dto.UserUpdateDto
import org.gameyfin.app.users.enums.RoleAssignmentResult
import jakarta.annotation.security.PermitAll import jakarta.annotation.security.PermitAll
import jakarta.annotation.security.RolesAllowed import jakarta.annotation.security.RolesAllowed
import org.gameyfin.app.core.Role import org.gameyfin.app.core.Role
import org.gameyfin.app.users.dto.UserInfoDto import org.gameyfin.app.users.dto.UserInfoDto
import org.gameyfin.app.users.dto.UserUpdateDto
import org.gameyfin.app.users.enums.RoleAssignmentResult
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
@@ -23,8 +23,7 @@ class UserEndpoint(
@PermitAll @PermitAll
fun getUserInfo(): UserInfoDto { fun getUserInfo(): UserInfoDto {
val auth: Authentication = SecurityContextHolder.getContext().authentication return userService.getUserInfo()
return userService.getUserInfo(auth)
} }
@PermitAll @PermitAll
@@ -1,25 +1,20 @@
package org.gameyfin.app.users package org.gameyfin.app.users
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gameyfin.app.config.ConfigProperties
import org.gameyfin.app.config.ConfigService import org.gameyfin.app.config.ConfigService
import org.gameyfin.app.core.Role
import org.gameyfin.app.core.Utils
import org.gameyfin.app.core.events.*
import org.gameyfin.app.games.entities.Image import org.gameyfin.app.games.entities.Image
import org.gameyfin.app.media.ImageService
import org.gameyfin.app.users.dto.UserInfoDto
import org.gameyfin.app.users.dto.UserRegistrationDto
import org.gameyfin.app.users.dto.UserUpdateDto import org.gameyfin.app.users.dto.UserUpdateDto
import org.gameyfin.app.users.emailconfirmation.EmailConfirmationService import org.gameyfin.app.users.emailconfirmation.EmailConfirmationService
import org.gameyfin.app.users.enums.RoleAssignmentResult import org.gameyfin.app.users.enums.RoleAssignmentResult
import org.gameyfin.app.users.persistence.UserRepository import org.gameyfin.app.users.persistence.UserRepository
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gameyfin.app.config.ConfigProperties
import org.gameyfin.app.core.Role
import org.gameyfin.app.core.Utils
import org.gameyfin.app.core.events.AccountDeletedEvent
import org.gameyfin.app.core.events.AccountStatusChangedEvent
import org.gameyfin.app.core.events.EmailNeedsConfirmationEvent
import org.gameyfin.app.core.events.RegistrationAttemptWithExistingEmailEvent
import org.gameyfin.app.core.events.UserRegistrationWaitingForApprovalEvent
import org.gameyfin.app.media.ImageService
import org.gameyfin.app.users.dto.UserInfoDto
import org.gameyfin.app.users.dto.UserRegistrationDto
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
@@ -66,7 +61,8 @@ class UserService(
fun existsByUsername(username: String): Boolean = userRepository.existsByUsername(username) fun existsByUsername(username: String): Boolean = userRepository.existsByUsername(username)
fun existsByEmail(email: String): Boolean = userRepository.existsByEmail(email) fun existsByEmail(email: String): Boolean = userRepository.existsByEmail(email)
fun findByOidcProviderId(oidcProviderId: String): org.gameyfin.app.users.entities.User? = userRepository.findByOidcProviderId(oidcProviderId) fun findByOidcProviderId(oidcProviderId: String): org.gameyfin.app.users.entities.User? =
userRepository.findByOidcProviderId(oidcProviderId)
fun getAllUsers(): List<UserInfoDto> { fun getAllUsers(): List<UserInfoDto> {
return userRepository.findAll().map { u -> toUserInfo(u) } return userRepository.findAll().map { u -> toUserInfo(u) }
@@ -84,7 +80,8 @@ class UserService(
return userRepository.findByUsername(username) ?: throw UsernameNotFoundException("Unknown user '$username'") return userRepository.findByUsername(username) ?: throw UsernameNotFoundException("Unknown user '$username'")
} }
fun getUserInfo(auth: Authentication): UserInfoDto { fun getUserInfo(): UserInfoDto {
val auth = SecurityContextHolder.getContext().authentication
val principal = auth.principal val principal = auth.principal
if (principal is OidcUser) { if (principal is OidcUser) {
@@ -99,6 +96,15 @@ class UserService(
return toUserInfo(user) return toUserInfo(user)
} }
fun getCurrentUser(): org.gameyfin.app.users.entities.User {
val auth = SecurityContextHolder.getContext().authentication
if (auth.principal is OidcUser) {
return userRepository.findByOidcProviderId((auth.principal as OidcUser).subject)
?: throw UsernameNotFoundException("OIDC user not found")
}
return getByUsernameNonNull(auth.name)
}
fun getAvatar(username: String): Image? { fun getAvatar(username: String): Image? {
val user = getByUsernameNonNull(username) val user = getByUsernameNonNull(username)
return user.avatar return user.avatar
@@ -174,7 +180,10 @@ class UserService(
} }
} }
fun registerUserFromInvitation(registration: UserRegistrationDto, email: String): org.gameyfin.app.users.entities.User { fun registerUserFromInvitation(
registration: UserRegistrationDto,
email: String
): org.gameyfin.app.users.entities.User {
val user = org.gameyfin.app.users.entities.User( val user = org.gameyfin.app.users.entities.User(
username = registration.username, username = registration.username,
password = passwordEncoder.encode(registration.password), password = passwordEncoder.encode(registration.password),