From 2d8392a40412b3457ccb4f6e1ba47ea337a9d0b7 Mon Sep 17 00:00:00 2001 From: Kowalski Date: Tue, 15 Sep 2026 17:22:53 +0200 Subject: [PATCH] fix(auth): reject premium login when stored Mojang UUID differs An existing premium user was matched by username only. After a username was released and reclaimed by another premium account, the new owner logged into the previous user's identity and inherited its permissions. Compare the stored mojangUuid with the Mojang-verified profile and deny the login on mismatch. --- .../auth/username/UsernameResFailureReason.kt | 3 +++ .../auth/username/UsernameResolutionService.kt | 12 ++++++++++++ .../navauth/common/config/MessagesConfig.kt | 6 ++++++ .../velocity/listener/velocity/LoginListeners.kt | 16 ++++++++++++++++ 4 files changed, 37 insertions(+) diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResFailureReason.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResFailureReason.kt index ab7c7a9..79fbee8 100644 --- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResFailureReason.kt +++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResFailureReason.kt @@ -29,5 +29,8 @@ sealed class UsernameResFailureReason { data class NonPremiumUsernameNotIdentical(val requiredUsername: String) : UsernameResFailureReason() + data class PremiumUsernameOwnedByAnotherAccount(val username: String) : + UsernameResFailureReason() + data object ProfileAPIFailure : UsernameResFailureReason() } diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResolutionService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResolutionService.kt index 3b3738a..91823bb 100644 --- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResolutionService.kt +++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResolutionService.kt @@ -112,6 +112,18 @@ constructor(private val userService: UserService, private val profileService: Pr } if (existingUserIgnoreCase != null && existingUserIgnoreCase.isPremium) { + // A premium account is bound to its Mojang UUID, not to its username. When the name is a + // premium name but the Mojang-verified UUID differs from the stored one, this is a DIFFERENT + // account that only re-claimed a released username - it must not be logged into the stored + // account. Without this guard a released-then-reclaimed username hands the previous owner's + // identity (and its permissions) to the new holder. + if (isPremiumNickname && existingUserIgnoreCase.mojangUuid != correspondingPremiumProfile.uuid) { + return failure( + UsernameResFailureReason.PremiumUsernameOwnedByAnotherAccount( + correspondingPremiumProfile.name.value + ) + ) + } if (connUsername.value != existingUserIgnoreCase.username.value) { return failure( UsernameResFailureReason.PremiumUsernameNotIdentical( diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt index 66db35d..d26be9b 100644 --- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt +++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt @@ -55,6 +55,12 @@ open class MessagesConfig : OkaeriConfig() { "Username '%USERNAME%' is already taken! Administrator needs to resolve the conflict." ) + var premiumUsernameOwnedByAnotherAccountError = + TextComponent( + "This username '%USERNAME%' belongs to a different premium account and cannot be used " + + "to log in here.
It was registered by a previous owner before the name was released." + ) + @Comment("Usually caused by API rate limit.") var profileApiFailureKickMessage = TextComponent( diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt index 411ff62..a84d955 100644 --- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt @@ -117,6 +117,9 @@ constructor( is UsernameResFailureReason.PremiumUsernameNotIdentical -> { premiumUsernameRequiredDeniedResult(connUsername, failureReason.requiredUsername) } + is UsernameResFailureReason.PremiumUsernameOwnedByAnotherAccount -> { + premiumUsernameOwnedByAnotherAccountDeniedResult(failureReason.username) + } is UsernameResFailureReason.NonPremiumWithPremiumConflict -> { usernameConflictDeniedResult(failureReason.premiumUsername) } @@ -290,6 +293,19 @@ constructor( return PreLoginEvent.PreLoginComponentResult.denied(component) } + private fun premiumUsernameOwnedByAnotherAccountDeniedResult( + username: String + ): PreLoginEvent.PreLoginComponentResult { + val comp = + withSupportFooter( + componentWithUsernamePlaceholder( + messagesConfig.premiumUsernameOwnedByAnotherAccountError, + username, + ) + ) + return PreLoginEvent.PreLoginComponentResult.denied(comp) + } + private fun usernameConflictDeniedResult( connUsername: String ): PreLoginEvent.PreLoginComponentResult {