gotify/server · warning
the user %s is already bound to a different OIDC identity
Error message
the user %s is already bound to a different OIDC identity
What it means
linkExistingUser rejects the login with HTTP 403 when the matching local user's OIDCID is already set to a different OIDC identity. Each local account may be bound to exactly one OIDC identity; binding a second identity would allow identity hijacking, so it is refused.
Source
Thrown at api/oidc.go:491
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 {
return nil, http.StatusForbidden, errors.New("user does not exist and auto-registration is disabled")
}
user := &model.User{
Name: username,View on GitHub (pinned to 14bfc25627)
Solutions
- Unbind the old OIDC ID from the local account (admin action/DB) so the new identity can bind
- Use a unique username claim (e.g. email or a dedicated UPN) so identities don't collide
- Check whether the IdP rotated sub values and migrate mappings accordingly
- Deduplicate IdP accounts that share the same username
Example fix
// before (shared mailbox claim) OIDC_USERNAME_CLAIM=mail // after (unique identity claim) OIDC_USERNAME_CLAIM=preferred_username
Defensive patterns
Strategy: validation
Validate before calling
// pre-check whether the username is already bound to another identity
u, _ := db.GetUserByName(username)
if u != nil && u.OIDCID != nil && *u.OIDCID != incomingOIDCID {
// resolve collision before login: unbind old identity or rename
} Type guard
func isBoundToOther(u *model.User, oidcID string) bool {
return u.OIDCID != nil && *u.OIDCID != oidcID
} Try / catch
user, status, err := resolveUser(...)
if status == http.StatusForbidden && strings.Contains(err.Error(), "already bound to a different OIDC identity") {
// trigger admin unbind flow or prompt for a distinct IdP account
http.Error(w, "username bound to another identity", http.StatusForbidden)
return
} Prevention
- Use a unique-per-user claim (preferred_username, UPN, email) as username
- Avoid shared/service mailboxes as OIDC usernames
- Track IdP sub rotation and provide an admin unbind action
- Deduplicate IdP accounts sharing one username
When it happens
Trigger: The username claim matches an existing user whose OIDCID != nil and *OIDCID != the incoming oidcID — e.g. two IdP accounts share one email/username, or the IdP changed the subject identifier.
Common situations: IdP re-provisioned a user with a new sub; a shared mailbox/service account used by multiple IdP identities; test accounts reused across different OIDC providers; usernames recycled in Active Directory after offboarding.
Related errors
- user is not in any allowed group
- a local user with the username %s already exists and linking
- user does not exist and auto-registration is disabled
- groups claim %q is missing
- groups claim %q contains a non-string element: %#v
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/ea3cae46cd2e57be.
Report an issue: GitHub.