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 {