Skip to content

fix(channels): make the documented Slack variables real, and stop four OAuth URLs from fabricating a client identifier - #17

Open
JOY (JOY) wants to merge 1 commit into
devfrom
fix/slack-env-and-oauth
Open

JOY (JOY) wants to merge 1 commit into
devfrom
fix/slack-env-and-oauth

Conversation

@JOY

@JOY JOY (JOY) commented Sep 14, 2026

Copy link
Copy Markdown

What this fixes

Three defects in the same area, all found while answering whether the documented environment variables were complete.

1. The Slack variables in .env.example were never read

SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET were documented as the Slack configuration, but the only one any code touched was SLACK_CLIENT_ID, read in the OAuth URL handler. The bot token and signing secret live on the channel, so setting the documented variables did nothing at all.

They are real bindings now, following the pattern Discord already had: a SlackConfig section with deployment-wide defaults, and a channel credential that wins over it. That matches how the Meta credentials resolve (#8), so a deployment with one Slack app has nothing to change and a deployment with several workspaces can still use per-channel credentials.

2. Four OAuth authorization URLs fabricated a client identifier

Channel Fabricated value
Discord 123456789012345678
Slack 123456789012.1234567890123
X x_oauth_client_id_placeholder
TikTok tiktok_client_key_placeholder

Each sent the operator to the provider's own error page, which reads as an application bug, and the real cause was never visible. They now answer which variable to set instead.

3. Slack webhook verification hardened

Matches the behaviour already contributed upstream in huabeitech#42: a delivery with no signature headers is rejected rather than waved through when a signing secret resolves for the channel, and request timestamps outside a five minute window are rejected. Slack signs the timestamp and the body but nothing in the signature expires, so without the check a captured request replays indefinitely.

A channel with no signing secret still accepts deliveries, so this is not breaking for an existing installation.

Slack OAuth install flow

POST /api/dashboard/channel/slack_oauth_callback exchanges an installation code through oauth.v2.access, verifies the resulting token with auth.test, reports the workspace identity and the preselected default channel, and saves everything onto the target channel while preserving its signing secret.

The token type and the chat:write scope are checked, because Slack does not treat a user token or a missing scope as an install error and either would leave a bot that cannot reply.

Tests

File Covers
internal/slack/client_test.go oauth.v2.access is form-encoded; ok:false surfaces as an error rather than being treated as success; blank redirect_uri omitted; auth.test failure surfaces; threaded replies carry thread_ts; a top-level post omits it
internal/services/slack_oauth_service_test.go connect persists workspace identity and bot token while preserving the existing signing secret; missing client credentials; a Slack-rejected code carries its reason; user token and missing chat:write are warnings rather than silent installs; auth.test failure does not discard a token Slack issued; non-Slack channels rejected
internal/services/slack_inbound_service_test.go unsigned delivery rejected and stores nothing; replayed timestamps rejected at 10 min, 1 hour and 10 min in the future; fresh and 4-minute-old signatures still accepted
internal/pkg/config resolvers return channel-level values when no configuration is loaded

Verification

  • go build ./..., go vet on every changed package: clean
  • go test ./internal/services/ ./internal/slack/ ./internal/pkg/config/ ./internal/handlers/dashboard/: all green
  • .env.example gains X and TikTok sections, whose client identifiers are read by the OAuth URL handlers and were undocumented; notes that LINE and Viber store everything per channel
  • 3 new backend i18n keys added to both en-US and zh-CN

Not included

The Slack frontend install button and landing page. The backend endpoint and route exist; wiring the popup flow needs the WhatsApp callback page generalized first, and is left as the next step.

web/messages/vi-VN.json is deliberately untouched here — it belongs to #16.


Note

Medium Risk
Changes channel OAuth error behavior, adds Slack token exchange and persistence, and tightens webhook signature checks when a signing secret is configured—security-sensitive but scoped and covered by tests.

Overview
Makes documented Slack deployment variables (SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET) load through SlackConfig and ResolveSlack, with per-channel bot token and signing secret overriding deployment defaults for inbound, outbound, and OAuth URL generation.

Adds POST /slack_oauth_callback and SlackOAuthService.Connect to exchange an install code via oauth.v2.access, optionally verify with auth.test, return workspace metadata for the channel form, and persist credentials on an existing channel while keeping the existing signing secret.

Discord, Slack, X, and TikTok OAuth URL handlers no longer substitute placeholder client IDs; they return a clear i18n error naming the env var and require redirect_uri. .env.example documents Slack, X, TikTok, and notes LINE/Viber are channel-only.

Slack Events API verification is tightened when a signing secret resolves: missing signature headers are rejected, timestamps outside five minutes are rejected, and outbound sending can fall back to SLACK_BOT_TOKEN when the channel has no token.

Reviewed by Cursor Bugbot for commit c25c573. Configure here.

… URLs from inventing credentials

Three defects in the same area, all found while answering whether the documented
environment variables were complete.

The Slack variables in .env.example were never read. SLACK_CLIENT_ID,
SLACK_CLIENT_SECRET, SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET were documented as
the Slack configuration, but the only one any code touched was SLACK_CLIENT_ID,
read in the OAuth URL handler. The bot token and signing secret live on the
channel, so setting the documented variables did nothing at all. They are real
bindings now, following the same pattern Discord already had: a SlackConfig
section with deployment-wide defaults, and a channel credential that wins over
it. That matches how the Meta credentials resolve, so a deployment with one Slack
app has nothing to change and a deployment with several workspaces can still use
per-channel credentials.

Four OAuth authorization URLs fabricated a client identifier when none was
configured. Discord answered with "123456789012345678", Slack with
"123456789012.1234567890123", X with "x_oauth_client_id_placeholder" and TikTok
with "tiktok_client_key_placeholder". Each sent the operator to the provider's
own error page, which reads as an application bug, and the real cause was never
visible. They now answer which variable to set instead.

Slack webhook verification also hardened, matching the behaviour already
contributed upstream: a delivery with no signature headers is rejected rather
than waved through when a signing secret resolves for the channel, and request
timestamps outside a five minute window are rejected. Slack signs the timestamp
and the body but nothing in the signature expires, so without the check a
captured request replays indefinitely. A channel with no signing secret still
accepts deliveries, so this is not breaking for an existing installation.

Slack's OAuth install flow is implemented: POST
/api/dashboard/channel/slack_oauth_callback exchanges an installation code
through oauth.v2.access, verifies the resulting token with auth.test, reports the
workspace identity and the preselected default channel, and saves everything onto
the target channel while preserving its signing secret. The token type and the
chat:write scope are checked, because Slack does not treat a user token or a
missing scope as an install error and either would leave a bot that cannot reply.

Masking of channel credentials is shared between the WhatsApp and Slack flows
rather than duplicated.

Tests

  internal/slack/client_test.go
      oauth.v2.access is form-encoded, ok:false surfaces as an error rather than
      being treated as success, a blank redirect_uri is omitted, auth.test failure
      surfaces, threaded replies carry thread_ts and a top-level post omits it
  internal/services/slack_oauth_service_test.go
      connect persists the workspace identity and the bot token while preserving
      the existing signing secret; missing client credentials is an error; a
      Slack-rejected code carries its reason through; a user token and a missing
      chat:write scope are warnings rather than silent installs; auth.test
      failure does not discard a token Slack issued; non-Slack channels rejected
  internal/services/slack_inbound_service_test.go
      an unsigned delivery is rejected and stores nothing, a replayed timestamp
      is rejected at ten minutes, one hour and ten minutes in the future, and a
      fresh or four-minute-old signature is still accepted
  internal/pkg/config
      the resolvers return channel-level values when no configuration is loaded

.env.example gains sections for X and TikTok, whose client identifiers are read
by the OAuth URL handlers and were undocumented, and notes that LINE and Viber
store everything per channel.
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 5cd60831-ac5b-4f03-aaea-16381e386214

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Sep 14, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_7a53bf5b-3347-4acb-a39b-742aaa1a30c8)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces Slack OAuth 2.0 integration, enabling the exchange of installation codes for workspace bot credentials, and adds deployment-wide Slack configuration support with channel-specific overrides. It also enhances webhook security by verifying Slack signatures with a five-minute timestamp tolerance to prevent replay attacks. Feedback on the changes highlights an inconsistency where the Connect callback service lacks the environment variable fallback for SLACK_CLIENT_ID and SLACK_CLIENT_SECRET present in the authorization URL handler, recommending that this fallback be added along with the necessary "os" import.

Comment on lines +64 to +72
app := config.ResolveSlack("", "")
if app.ClientID == "" || app.ClientSecret == "" {
return nil, errorsx.InvalidParamI18n("error.slack.oauth.clientCredentialsMissing")
}

ctx, cancel := context.WithTimeout(context.Background(), slackOAuthTimeout)
defer cancel()

exchange, err := slackExchangeOAuthCode(ctx, slackOAuthBaseURL, app.ClientID, app.ClientSecret, code, req.RedirectURI)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The OAuth URL handler (ChannelGetSlackOAuthURL) falls back to reading SLACK_CLIENT_ID from the environment via os.Getenv if it is not resolved from the configuration. However, the Connect callback service does not implement this fallback, which can lead to a situation where the authorization flow is initiated successfully but the callback fails with a clientCredentialsMissing error.

We should add the same fallback to os.Getenv for both SLACK_CLIENT_ID and SLACK_CLIENT_SECRET in the Connect method to ensure consistent behavior.

	app := config.ResolveSlack("", "")
	clientID := app.ClientID
	if clientID == "" {
		clientID = strings.TrimSpace(os.Getenv("SLACK_CLIENT_ID"))
	}
	clientSecret := app.ClientSecret
	if clientSecret == "" {
		clientSecret = strings.TrimSpace(os.Getenv("SLACK_CLIENT_SECRET"))
	}
	if clientID == "" || clientSecret == "" {
		return nil, errorsx.InvalidParamI18n("error.slack.oauth.clientCredentialsMissing")
	}

	ctx, cancel := context.WithTimeout(context.Background(), slackOAuthTimeout)
	defer cancel()

	exchange, err := slackExchangeOAuthCode(ctx, slackOAuthBaseURL, clientID, clientSecret, code, req.RedirectURI)

Comment on lines +3 to +8
import (
"context"
"encoding/json"
"log/slog"
"strings"
"time"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add the "os" package to the imports so we can access environment variables using os.Getenv.

Suggested change
import (
"context"
"encoding/json"
"log/slog"
"strings"
"time"
import (
"context"
"encoding/json"
"log/slog"
"os"
"strings"
"time"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant