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), nilView on GitHub (pinned to 2c0912b6ec)
Solutions
- 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.
- Verify which account currently holds the row (query lark_user_binding by installation + open_id) before re-attempting the link.
- 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
- Never reuse a Lark identity across Multica accounts in tests or scripts; bind one open_id to one account.
- Surface the unbind flow in the UI wherever binding status is shown, so transfer has a sanctioned path.
- Log (installation_id, open_id) context with this error to identify the owning account quickly.
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
- slack: user id is already bound to a different user
- wecom: user id is already bound to a different user
- redeemer is not a workspace member
- ErrInstallationNotFound
- slack: binding token invalid or expired
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/147d708b6b8b1398.
Report an issue: GitHub.