gotify/server · critical

failed to bind user to OIDC identity

Error message

failed to bind user to OIDC identity: %w

What it means

linkExistingUser wraps a failure from DB.UpdateUser while persisting the newly bound OIDCID (and possibly the Admin flag) as 'failed to bind user to OIDC identity: %w' with HTTP 500. Authentication and lookup succeeded, but the binding write failed so the link is not established.

Solutions

  1. Read the wrapped driver error in logs and fix the underlying storage issue
  2. Verify the users table schema supports the oidc_id column (run migrations)
  3. Retry the login; the next attempt will re-run the link
  4. Avoid concurrent first logins for the same account during migration
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure schema supports oidc_id before enabling OIDC
var count int
_ = db.QueryRow("SELECT COUNT(*) FROM information_schema.columns WHERE table_name='users' AND column_name='oidc_id'").Scan(&count)
if count == 0 { return errors.New("run migrations: users.oidc_id missing") }

Try / catch

user, status, err := resolveUser(...)
if err != nil && strings.Contains(err.Error(), "failed to bind user to OIDC identity") {
    log.Error().Err(err).Msg("OIDC bind failed; check DB")
    http.Error(w, "temporary server error", http.StatusInternalServerError)
    return
}

Prevention

When it happens

Trigger: user.OIDCID = &oidcID and optional Admin update are set, then a.DB.UpdateUser(user) returns an error during a link-by-username login.

Common situations: DB connectivity loss; optimistic-lock/concurrent-update conflicts when the same user logs in from two sessions; NOT NULL/length constraints on oidc_id column; schema not migrated for the OIDCID column.

Related errors


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

Appendix: source

Thrown at api/oidc.go:498

	}
	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,
		Pass:   nil,
		OIDCID: &oidcID,
	}

	if len(a.GroupsAdmin) > 0 {
		user.Admin = hasAdminGroup
	}

View on GitHub (pinned to 14bfc25627)