multica-ai/multica · error · ErrBindingAlreadyAssigned

lark open_id is already bound to a different user

Error message

lark open_id is already bound to a different user

What it means

Lark binding sentinel error: RedeemAndBind found an existing lark_user_binding row for the (installation, open_id) pair that points at a DIFFERENT Multica user. The library deliberately blocks this path because a binding token must not be usable to hijack an already-linked Lark identity from another account. Account transfer requires the explicit unbind flow instead.

Source

Thrown at server/internal/integrations/lark/binding_token.go:280

		return fmt.Errorf("bind installer: %w", err)
	}
	return nil
}

// ErrBindingTokenInvalid is returned by RedeemAndBind when the token
// hash does not exist, the token has already been consumed, or it
// has expired. The caller must NOT distinguish those sub-cases —
// that distinction enables timing oracles for token replay races and
// adds no product value (the user sees the same "link invalid or
// expired, please request a new one" copy either way).
var ErrBindingTokenInvalid = errors.New("binding token invalid or expired")

// ErrBindingAlreadyAssigned is returned by RedeemAndBind when a
// lark_user_binding row already exists for the (installation,
// open_id) pair and points at a different Multica user. Account
// transfer must go through an explicit unbind flow; a binding token
// cannot be used to grab an already-bound open_id from another user.
var ErrBindingAlreadyAssigned = errors.New("lark open_id is already bound to a different user")

// ErrBindingNotWorkspaceMember is returned by RedeemAndBind and
// BindInstallerTx when the user is not (or no longer) a member of the
// target workspace, detected by an explicit IsWorkspaceMember check
// (MUL-3515 §4 removed the member FK that used to enforce this).
// Translated to 403 at the HTTP boundary.
var ErrBindingNotWorkspaceMember = errors.New("redeemer is not a workspace member")

func randomToken(n int) (string, error) {
	buf := make([]byte, n)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	// URL-safe so the token embeds cleanly in the binding URL
	// without escaping. RawURLEncoding drops `=` padding which is
	// optional for decoders and would otherwise look ugly in
	// user-visible URLs.
	return base64.RawURLEncoding.EncodeToString(buf), nil

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Sign in to the Multica account that already owns the binding and run the explicit unbind flow for the Lark open_id, then mint a fresh binding token and redeem it with the intended account.
  2. Verify which account currently holds the row (query lark_user_binding by installation + open_id) before re-attempting the link.
  3. If the previous owner is defunct, have an admin remove the stale lark_user_binding row, then retry redemption with a new token (old tokens are single-use anyway).

Example fix

// before: redeeming while the open_id is bound to another user
err := larkSvc.RedeemAndBind(ctx, token, currentUser)
// -> "lark open_id is already bound to a different user"

// after: unbind first, then redeem a fresh token
if err := larkSvc.RedeemAndBind(ctx, token, currentUser); err != nil {
	if errors.Is(err, lark.ErrBindingAlreadyAssigned) {
		// route the user to the explicit unbind flow (owner must do it)
		renderUnbindRequired(w)
		return
	}
	// handle other errors
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before redeeming, check the current owner of the (installation, open_id)
owner, err := store.GetBindingOwner(ctx, installationID, openID)
if err == nil && owner != redeemingUserID {
	// route to the explicit unbind flow instead of redeeming
	return renderUnbindRequired(w)
}
err = larkSvc.RedeemAndBind(ctx, token, redeemingUser)

Try / catch

res, err := larkSvc.RedeemAndBind(ctx, rawToken, user)
if err != nil {
	switch {
	case errors.Is(err, lark.ErrBindingAlreadyAssigned):
		// do NOT retry with the same token; require unbind flow
		return respondConflict(w, "open_id bound to another user — unbind first")
	default:
		return err
	}
}

Prevention

When it happens

Trigger: Calling the lark BindingTokenService RedeemAndBind (the in-product redeem page reached from a binding link) when the Lark open_id in the token is already bound to another Multica user in that installation. Typical when two Multica accounts claim the same Lark user, or a user previously linked their Lark account and later tries to redeem a binding link while signed in as a second Multica account.

Common situations: A user switched Multica accounts and re-clicks an old binding message link; QA reuses one Lark test account against several Multica test users; a teammate redeems someone else's link. Also hit after account splits/restores that leave stale lark_user_binding rows.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/147d708b6b8b1398. Report an issue: GitHub.