{"record":{"id":"9c4699cd4ee95d6f","repo":"multica-ai/multica","slug":"slack-redeemer-is-not-a-workspace-member","errorCode":null,"errorMessage":"slack: redeemer is not a workspace member","messagePattern":"slack: redeemer is not a workspace member","errorType":"http","errorClass":"ErrBindingNotWorkspaceMember","httpStatus":403,"severity":"error","filePath":"server/internal/integrations/slack/binding.go","lineNumber":40,"sourceCode":"// the OutboundReplier), clicks through to the in-product redeem page, and their\n// Slack user id is bound to their Multica account. It mirrors\n// lark.BindingTokenService but runs on the generic channel_* queries with\n// channel_type='slack' (lark's ChannelStore hardcodes 'feishu').\n\n// BindingTokenTTL bounds a token's life. The channel_binding_token CHECK\n// enforces the same 15-minute cap so a misconfigured caller cannot mint longer.\nconst BindingTokenTTL = 15 * time.Minute\n\nvar (\n\t// ErrBindingTokenInvalid: token unknown / already consumed / expired. One\n\t// opaque error for all three avoids a replay timing oracle.\n\tErrBindingTokenInvalid = errors.New(\"slack: binding token invalid or expired\")\n\t// ErrBindingAlreadyAssigned: this Slack user id is already bound to a\n\t// different Multica user (account transfer must go through explicit unbind).\n\tErrBindingAlreadyAssigned = errors.New(\"slack: user id is already bound to a different user\")\n\t// ErrBindingNotWorkspaceMember: the redeemer is not a member of the token's\n\t// workspace. Translated to 403 at the HTTP boundary.\n\tErrBindingNotWorkspaceMember = errors.New(\"slack: redeemer is not a workspace member\")\n)\n\n// BindingToken is a freshly minted token. The raw value is returned exactly\n// once (embedded in the binding URL); only its hash is persisted.\ntype BindingToken struct {\n\tRaw       string\n\tExpiresAt time.Time\n}\n\n// RedeemedBindingToken is returned after a successful redemption.\ntype RedeemedBindingToken struct {\n\tWorkspaceID    pgtype.UUID\n\tInstallationID pgtype.UUID\n\tSlackUserID    string\n}\n\n// BindingTokenService mints and redeems Slack binding tokens. Redemption is\n// transactional: consuming the token and inserting the channel_user_binding row","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/integrations/slack/binding.go#L22-L58","documentation":"Slack binding sentinel error: the user redeeming the binding token is not a member of the token's workspace. Detected by an explicit membership check during redemption; the HTTP boundary maps it to 403.","triggerScenarios":"Redeeming a Slack binding token when the redeemer has no membership row in the workspace the token was minted for — e.g. removed from the workspace mid-flow, or signed into a different Multica account that never belonged to that workspace.","commonSituations":"Admin kicks the user during the token's 15-minute life and the user then clicks the DM link; multi-workspace users redeeming a link while the wrong workspace/account is active in the session.","solutions":["Have a workspace admin re-add the redeeming user, then request a fresh binding link (the old one may have expired).","Make sure the redeem page session matches a user account that is a member of the token's workspace.","Check membership rows for the (workspace, user) pair if the user believes they are a member — stale session data can mask a removal."],"exampleFix":"// before\nres, err := svc.Redeem(ctx, rawToken)\nif err != nil {\n\thttp.Error(w, err.Error(), 500)\n}\n\n// after\nres, err := svc.Redeem(ctx, rawToken)\nif errors.Is(err, slack.ErrBindingNotWorkspaceMember) {\n\thttp.Error(w, \"you must be a member of this workspace\", http.StatusForbidden)\n\treturn\n}","handlingStrategy":"validation","validationCode":"if !members.IsMember(ctx, token.WorkspaceID, user.ID) {\n\treturn respondForbidden(w, \"join the workspace before linking Slack\")\n}\n_ = slackSvc.Redeem(ctx, rawToken)","typeGuard":null,"tryCatchPattern":"if err := slackSvc.Redeem(ctx, rawToken); err != nil {\n\tif errors.Is(err, slack.ErrBindingNotWorkspaceMember) {\n\t\treturn respondStatus(w, http.StatusForbidden)\n\t}\n\treturn err\n}","preventionTips":["Re-validate membership at redemption time, not just at link-mint time.","Show which workspace a binding link belongs to before the user commits.","Treat removal-from-workspace during a binding flow as an expected race with a friendly 403."],"tags":["slack","binding","authorization","workspace","go"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}