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

  1. Unbind the old OIDC ID from the local account (admin action/DB) so the new identity can bind
  2. Use a unique username claim (e.g. email or a dedicated UPN) so identities don't collide
  3. Check whether the IdP rotated sub values and migrate mappings accordingly
  4. 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

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


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/ea3cae46cd2e57be. Report an issue: GitHub.