gotify/server · warning
a local user with the username %s already exists and linking
Error message
a local user with the username %s already exists and linking by username is disabled
What it means
linkExistingUser rejects the login with HTTP 403 when an existing local user has the same username but LinkByUsername is disabled. The library will not silently attach an OIDC identity to a pre-existing local account without explicit opt-in, preventing account takeover via a colliding username claim.
Source
Thrown at api/oidc.go:487
username := fmt.Sprint(usernameRaw)
if username == "" || usernameRaw == nil {
return nil, http.StatusInternalServerError, errors.New("username claim was empty")
}
byUsername, err := a.DB.GetUserByName(username)
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("database error: %w", err)
}
if byUsername != nil {
return a.linkExistingUser(byUsername, oidcID, hasAdminGroup)
}
return a.registerUser(username, oidcID, hasAdminGroup)
}
func (a *OIDCAPI) linkExistingUser(user *model.User, oidcID string, hasAdminGroup bool) (*model.User, int, error) {
if !a.LinkByUsername {
log.Warn().Str("oidc_id", oidcID).Str("username", user.Name).Msgf("OIDC login rejected: a local user with the username already exists and %s is disabled", config.EnvOIDCLinkByUsername)
return nil, http.StatusForbidden, fmt.Errorf("a local user with the username %s already exists and linking by username is disabled", user.Name)
}
if user.OIDCID != nil {
log.Warn().Str("oidc_id", oidcID).Str("bound_oidc_id", *user.OIDCID).Str("username", user.Name).Msg("OIDC login rejected: the username is already bound to a different OIDC identity")
return nil, http.StatusForbidden, fmt.Errorf("the user %s is already bound to a different OIDC identity", user.Name)
}
user.OIDCID = &oidcID
if len(a.GroupsAdmin) > 0 {
user.Admin = hasAdminGroup
}
if err := a.DB.UpdateUser(user); err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to bind user to OIDC identity: %w", err)
}
log.Warn().Str("oidc_id", oidcID).Str("username", user.Name).Bool("admin", user.Admin).Msg("OIDC link by username")
return user, 0, nil
}
func (a *OIDCAPI) registerUser(username, oidcID string, hasAdminGroup bool) (*model.User, int, error) {
if !a.AutoRegister {View on GitHub (pinned to 14bfc25627)
Solutions
- Enable linking by username (set the OIDC_LINK_BY_USERNAME=true env / LinkByUsername: true)
- Rename either the local user or the IdP username so they no longer collide
- Manually bind the OIDC ID to the local account via admin tooling/DB
- Pre-seed users via SCIM or provisioning so identities bind cleanly
Example fix
// before LinkByUsername: false // after LinkByUsername: true // or env OIDC_LINK_BY_USERNAME=true
Defensive patterns
Strategy: validation
Validate before calling
// check before enabling OIDC login for existing installs
existing, _ := db.GetUserByName(usernameFromToken)
if existing != nil && !linkByUsername {
// decide: enable linking or rename the account
} Try / catch
user, status, err := resolveUser(...)
if status == http.StatusForbidden && strings.Contains(err.Error(), "linking by username is disabled") {
// admin action required: enable LinkByUsername or rename the colliding user
http.Error(w, "account linking required", http.StatusForbidden)
return
} Prevention
- Enable OIDC_LINK_BY_USERNAME when migrating pre-existing local users
- Audit local usernames against IdP usernames before rollout
- Provision users via the IdP/SCIM to avoid drift
- Log the collision (the library already warns) and review regularly
When it happens
Trigger: First-time OIDC login where the username claim matches an existing local user's Name, GetUserByName returns that user, and a.LinkByUsername is false.
Common situations: Local users were created before OIDC was enabled with the same usernames as IdP accounts; OIDC_LINK_BY_USERNAME env left unset/false; IdP email/preferred_username collides with legacy local accounts.
Related errors
- user does not exist and auto-registration is disabled
- user is not in any allowed group
- the user %s is already bound to a different OIDC identity
- groups claim %q is missing
- issuer url %q is not a valid url: %w
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/9688c63ae98fa05d.
Report an issue: GitHub.