-
Notifications
You must be signed in to change notification settings - Fork 9
Fix "username already exists" errors when linking #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
* Update score validation to include subscore checks in statistics queue * Update leaderboard record write to include metadata mapping * Fix statistics storage with operator tags for leaderboard operations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR fixes "username already exists" errors when linking Discord accounts to game headsets by implementing conflict resolution logic. The solution detects when a username conflict occurs during authentication and attempts to recover by either linking the Discord ID to an existing unlinked account or identifying when the account is already properly linked.
Key changes:
- Adds intelligent conflict detection and recovery for username collisions during account linking
- Implements three recovery paths: already-linked accounts, unlinked accounts, and genuine conflicts
- Introduces error type checking to distinguish AlreadyExists errors from other authentication failures
| // Link the account to the current discord user | ||
| logger.WithFields(map[string]interface{}{ | ||
| "discord_id": discordID, | ||
| "conflicting_user_id": conflictingUserID, | ||
| "username": username, | ||
| }).Info("Attempting to recover by linking custom_id to conflicting account") | ||
|
|
||
| if err := d.linkCustomID(ctx, logger, conflictingUserID, discordID); err != nil { | ||
| logger.WithFields(map[string]interface{}{ | ||
| "discord_id": discordID, | ||
| "conflicting_user_id": conflictingUserID, | ||
| "error": err, | ||
| }).Error("Failed to link custom ID during conflict recovery") | ||
| return "", fmt.Errorf("failed to recover account: %w", err) | ||
| } | ||
|
|
||
| logger.WithFields(map[string]interface{}{ | ||
| "discord_id": discordID, | ||
| "user_id": conflictingUserID, | ||
| "username": username, | ||
| }).Info("Recovery successful: custom_id linked to conflicting account") | ||
|
|
||
| return conflictingUserID, nil |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The automatic linking of custom ID to a conflicting account (lines 100-122) occurs without explicit user verification. This could be a security concern if:
- An attacker creates an account with a victim's Discord username before the victim links
- The victim then tries to link, and the system automatically associates their Discord ID with the attacker's account
Consider adding additional verification (e.g., checking account creation date, requiring explicit user confirmation, or validating that the account has minimal activity) before automatically linking to prevent potential account takeover scenarios.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
discord usernames are unique
| // Also check the error message string for the AlreadyExists code | ||
| // Good for cases where the error is wrapped or stringified | ||
| errMsg := err.Error() | ||
| return strings.Contains(errMsg, "code = AlreadyExists") || strings.Contains(errMsg, "AlreadyExists") |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The string matching in isAlreadyExistsError checks for both "code = AlreadyExists" and "AlreadyExists" separately. The second check is redundant since any string containing "code = AlreadyExists" will also match "AlreadyExists". Consider simplifying to just check for "AlreadyExists" if the goal is to catch both formatted and unformatted error messages.
| return strings.Contains(errMsg, "code = AlreadyExists") || strings.Contains(errMsg, "AlreadyExists") | |
| return strings.Contains(errMsg, "AlreadyExists") |
| if conflictingCustomID != "" { | ||
| if conflictingCustomID == discordID { | ||
| // Return the existing user ID - they're already linked. | ||
| logger.WithFields(map[string]interface{}{ | ||
| "discord_id": discordID, | ||
| "user_id": conflictingUserID, | ||
| "username": username, | ||
| }).Info("Recovery successful: user already linked with matching custom_id") | ||
| return conflictingUserID, nil | ||
| } | ||
|
|
||
| // This is for cases where a different user owns the conflicting account. | ||
| // This cannot be automatically resolved. | ||
| logger.WithFields(map[string]interface{}{ | ||
| "discord_id": discordID, | ||
| "conflicting_user": conflictingCustomID, | ||
| "username": username, | ||
| }).Warn("Account conflict: different discord user owns the username") | ||
| return "", errors.New( | ||
| "Username is already linked to a different discord account. " + | ||
| "Reference: CONFLICT_DIFFERENT_OWNER", | ||
| ) | ||
| } | ||
|
|
||
| // Link the account to the current discord user | ||
| logger.WithFields(map[string]interface{}{ | ||
| "discord_id": discordID, | ||
| "conflicting_user_id": conflictingUserID, | ||
| "username": username, | ||
| }).Info("Attempting to recover by linking custom_id to conflicting account") | ||
|
|
||
| if err := d.linkCustomID(ctx, logger, conflictingUserID, discordID); err != nil { | ||
| logger.WithFields(map[string]interface{}{ | ||
| "discord_id": discordID, | ||
| "conflicting_user_id": conflictingUserID, | ||
| "error": err, | ||
| }).Error("Failed to link custom ID during conflict recovery") | ||
| return "", fmt.Errorf("failed to recover account: %w", err) | ||
| } | ||
|
|
||
| logger.WithFields(map[string]interface{}{ | ||
| "discord_id": discordID, | ||
| "user_id": conflictingUserID, | ||
| "username": username, | ||
| }).Info("Recovery successful: custom_id linked to conflicting account") | ||
|
|
||
| return conflictingUserID, nil |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential race condition between checking the conflicting account's custom_id (line 76) and linking it (line 107). Another process could link a different discord ID to this account between these operations. Consider using a transaction or atomic operation if the underlying Nakama API supports it, or at least re-verify the custom_id is still empty after the link operation succeeds.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Andrew Bates <a@sprock.io>
- Simplify isAlreadyExistsError to remove redundant string check - Add comprehensive security documentation for automatic linking - Document race condition risks and mitigations - Fix test compilation issue with operatorFromTag/operatorFromStatField Co-authored-by: thesprockee <962164+thesprockee@users.noreply.github.com>
Signed-off-by: Andrew Bates <a@sprock.io>
Fixes: #146