multica-ai/multica · error · ErrBindingTokenInvalid
slack: binding token invalid or expired
Error message
slack: binding token invalid or expired
What it means
Slack binding sentinel error (mirrors lark.BindingTokenService on the generic channel_* queries with channel_type='slack'): the token hash is unknown, already consumed, or past its 15-minute TTL (BindingTokenTTL, also enforced by a channel_binding_token CHECK). One opaque error for all three sub-cases deliberately, so callers cannot build a replay timing oracle.
Source
Thrown at server/internal/integrations/slack/binding.go:34
"github.com/multica-ai/multica/server/internal/integrations/channel/engine"
db "github.com/multica-ai/multica/server/pkg/db/generated"
)
// This file is the Slack user-binding token flow: an unbound Slack user who
// messages the bot gets a "link your account" prompt (minted here, delivered by
// the OutboundReplier), clicks through to the in-product redeem page, and their
// Slack user id is bound to their Multica account. It mirrors
// lark.BindingTokenService but runs on the generic channel_* queries with
// channel_type='slack' (lark's ChannelStore hardcodes 'feishu').
// BindingTokenTTL bounds a token's life. The channel_binding_token CHECK
// enforces the same 15-minute cap so a misconfigured caller cannot mint longer.
const BindingTokenTTL = 15 * time.Minute
var (
// ErrBindingTokenInvalid: token unknown / already consumed / expired. One
// opaque error for all three avoids a replay timing oracle.
ErrBindingTokenInvalid = errors.New("slack: binding token invalid or expired")
// ErrBindingAlreadyAssigned: this Slack user id is already bound to a
// different Multica user (account transfer must go through explicit unbind).
ErrBindingAlreadyAssigned = errors.New("slack: user id is already bound to a different user")
// ErrBindingNotWorkspaceMember: the redeemer is not a member of the token's
// workspace. Translated to 403 at the HTTP boundary.
ErrBindingNotWorkspaceMember = errors.New("slack: redeemer is not a workspace member")
)
// BindingToken is a freshly minted token. The raw value is returned exactly
// once (embedded in the binding URL); only its hash is persisted.
type BindingToken struct {
Raw string
ExpiresAt time.Time
}
// RedeemedBindingToken is returned after a successful redemption.
type RedeemedBindingToken struct {
WorkspaceID pgtype.UUIDView on GitHub (pinned to 2c0912b6ec)
Solutions
- Ask the bot for a fresh binding message (a new token is minted) and redeem promptly within the 15-minute TTL.
- Make redemption idempotent on the client: if the first attempt may have succeeded, verify the binding exists before re-redeeming the same token.
- Ensure the binding URL is transported unmodified (no truncation, no re-encoding) from Slack message to the redeem page.
Example fix
// before
bt, err := svc.Redeem(ctx, rawToken)
if err != nil {
return fmt.Errorf("redeem failed: %w", err) // opaque 500
}
// after
bt, err := svc.Redeem(ctx, rawToken)
if errors.Is(err, slack.ErrBindingTokenInvalid) {
// unknown / consumed / expired are indistinguishable by design:
// tell the user to request a new link
renderLinkExpired(w) // 400, "link invalid or expired, request a new one"
return
} Defensive patterns
Strategy: try-catch
Try / catch
bt, err := slackSvc.Redeem(ctx, rawToken)
if err != nil {
if errors.Is(err, slack.ErrBindingTokenInvalid) {
// do NOT distinguish unknown/consumed/expired (timing oracle by design)
return renderLinkExpired(w) // always "request a new link"
}
return err
} Prevention
- Open the binding link immediately; the TTL is 15 minutes and tokens are single-use.
- On the client, disable double-clicks and dedupe submit events around redemption.
- Never branch on sub-cases of this error — the library deliberately collapses them; do the same in your copy.
When it happens
Trigger: Redeeming a Slack binding token that has already been used (tokens are single-use: only the hash is persisted and redemption consumes it), that is older than 15 minutes, or whose raw value was mistyped/truncated from the binding URL. Any Redeem call with a hash absent from channel_binding_token yields exactly this error.
Common situations: User clicks a Slack DM binding link twice (second click after a refresh); user sits on the link longer than 15 minutes before clicking; link mangled by a proxy or chat client that truncates query params; double-submit from a flaky network retry.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- wecom: binding token invalid or expired
- slack: user id is already bound to a different user
- lark open_id is already bound to a different user
- slack: redeemer is not a workspace member
- slack installation not found
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/93ea65b3a6da0d0c.
Report an issue: GitHub.