Skip to content

feat: OAuth 2.0 multi-user support with GovSlack compatibility#166

Open
aron-muon wants to merge 12 commits intokorotovsky:masterfrom
aron-muon:aron/oauth-improvements
Open

feat: OAuth 2.0 multi-user support with GovSlack compatibility#166
aron-muon wants to merge 12 commits intokorotovsky:masterfrom
aron-muon:aron/oauth-improvements

Conversation

@aron-muon
Copy link
Contributor

@aron-muon aron-muon commented Jan 28, 2026

Summary

This PR adds OAuth 2.0 authentication for multi-user support, enabling the Slack MCP server to work with multiple users via OAuth tokens. It includes full GovSlack and Enterprise Grid compatibility.

Based on foundational work by @wentaoyang-moloco (PR #124), with additional enhancements for GovSlack environments and various fixes.

Core OAuth Implementation (from PR #124)

  • OAuth 2.0 authorization flow with Slack API
  • CSRF protection via state parameter validation
  • In-memory token storage with automatic cleanup
  • Per-request Slack client creation for token isolation
  • Support for both user tokens (xoxp) and bot tokens (xoxb)

Dual-Mode Architecture

  • Legacy mode: Single-user with browser tokens (xoxc/xoxd/xoxp)
  • OAuth mode: Multi-user with OAuth tokens
  • Fully backward compatible with existing configurations

OAuth Handlers

  • /oauth/authorize endpoint for initiating OAuth flow
  • /oauth/callback endpoint for processing OAuth callbacks
  • OAuth middleware for token validation and user context injection

GovSlack & External OAuth Enhancements

1. Workspace URL Capture and Usage

Problem: External tokens validated successfully via auth.test but subsequent API calls failed with invalid_auth because the Slack client pointed to the wrong API endpoint (default https://slack.com/api/ instead of workspace-specific URLs like GovSlack).

Solution:

  • Capture workspace URL from auth.test response
  • Propagate URL through user context
  • Use slack.OptionAPIURL() when creating Slack clients

2. HTTP-Level Authentication (401 Responses)

Problem: In OAuth mode, authentication errors were only returned as MCP-level errors, not proper HTTP status codes.

Solution:

  • Added OAuthHTTPMiddleware that wraps HTTP handlers with Slack token validation
  • Returns HTTP 401 Unauthorized for:
    • Missing Authorization header
    • Invalid header format (must be Bearer <token>)
    • Invalid Slack tokens (validated via auth.test)
  • OAuth endpoints (/oauth/authorize, /oauth/callback) and /health are excluded from authentication checks
  • Provides clear error messages in the response body

3. User Name Resolution in OAuth Mode

Problem: Messages displayed user IDs instead of usernames/display names.

Solution: Added fetchUsersForMessages() helper to resolve user info from Slack API.

4. Channel Name to ID Resolution

Problem: Users couldn't use channel names like #general - had to know internal channel IDs.

Solution: Added resolveChannelName() helper that handles #channel and @user prefixes with Slack API fallback.

5. DM Channel Display Improvements

Problem: DM channels showed # instead of usernames, member counts were 0.

Solution: Detect DM channels and fetch user info for proper @username display.

6. Slack Mention Text Processing

Problem: User mentions showed concatenated text like U1UEU5QF0Ava Kopp.

Solution: Added regex handlers for Slack mention formats (&lt;@U123&gt;@U123, &lt;@U123|name&gt;@name).

7. User Mention Expansion in Message Text

Problem: conversations_history showed raw user IDs in message text while conversations_search properly expanded to names.

Solution: Added expandUserMentions() function to expand &lt;@U123&gt; to @RealName using the users map.

8. Health Endpoint

Added /health endpoint that returns JSON status and version information. Available in all transport modes (stdio, sse, http) without requiring authentication.

Bug Fixes & Input Validation Improvements

1. URL Message Search

Problem: The conversations_search_messages tool documented URL-based message lookup but it wasn't implemented.

Solution: Added parseSlackMessageURL() to detect and parse Slack message URLs (e.g., https://slack.com/archives/C1234567890/p1234567890123456) and fetchSingleMessage() to retrieve the specific message directly from conversation history.

2. Invalid Cursor Handling

Problem: Invalid pagination cursors were silently ignored, returning results from the beginning instead of an error.

Solution: Updated paginateChannels() to validate cursor format and return proper errors for malformed cursors (base64 decode failures, invalid channel ID format).

3. Limit Validation

Problem: Various edge cases with limit parameter:

  • limit=0 returned all data instead of using default
  • Negative limits were accepted
  • Limits exceeding API maximums caused issues

Solution:

  • limit=0 now uses the default value (50 for history, 100 for search)
  • Negative limits return an error with clear message
  • Search limits capped at 100, channel limits capped at 999

4. Search Query Validation

Problem: Empty search queries or missing parameters returned confusing errors.

Solution: Added validation requiring either a search term or at least one filter parameter, with helpful error messages listing available options.

5. Channel Type Validation

Problem: Invalid channel types (e.g., channel_types=foo) were silently ignored.

Solution: Now returns an error listing valid types: public_channel, private_channel, im, mpim.

6. DM Name Resolution

Problem: Looking up DMs by display name with spaces (e.g., @John Smith) failed.

Solution: Added case-insensitive matching against username, display name, and real name.

7. User Prefix Validation

Problem: Bare @ without a username was accepted without error.

Solution: Now returns an error: '@' requires a username (e.g., @username).

8. Channel/User Mention Preservation

Problem: The text processor regex was stripping # and @ characters from message text, breaking channel mentions like #general.

Solution: Updated cleanRegex to preserve # and @ characters in processed text.

Architecture: OAuth Mode vs Legacy Mode

Caching Differences

Feature Legacy Mode OAuth Mode
User cache Pre-loaded at startup No cache - fetched per request
Channel cache Pre-loaded at startup No cache - fetched per request
API calls Minimal (uses cache) Additional calls for user/channel resolution
Startup time Longer (warming caches) Immediate
Memory usage Higher (cached data) Lower (no persistent cache)

Why no caching in OAuth mode?

In OAuth mode, each request comes from a different user with different permissions. Caching would require:

  • Per-user cache isolation
  • Cache invalidation across users
  • Significantly more memory for multi-tenant scenarios

Instead, OAuth mode makes targeted API calls to fetch the specific information needed:

  • users.info for resolving user IDs to names
  • conversations.list for channel name to ID resolution
  • conversations.info for channel details

This trade-off prioritizes correctness and permission isolation over raw performance.
Additional work can be included in future PRs for enabling a cache mode on Oauth - I imagine this would mean adding an intelligent caching mechanism like Redis or Memcached for a global reference of IDs to names.

Environment Variables

  • SLACK_MCP_OAUTH_ENABLED: Enable OAuth mode
  • SLACK_MCP_OAUTH_CLIENT_ID: Slack app client ID
  • SLACK_MCP_OAUTH_CLIENT_SECRET: Slack app client secret
  • SLACK_MCP_OAUTH_REDIRECT_URI: OAuth callback URL (requires HTTPS)

Testing

  • All existing tests pass
  • Manual testing with GovSlack OAuth tokens confirmed working
  • 401 responses verified for missing/invalid tokens
  • Input validation tested for edge cases

Breaking Changes

None. All changes are backward compatible.

Credits

Core OAuth implementation by @wentaoyang-moloco. GovSlack compatibility, bug fixes, and additional enhancements by @aron-muon.

@aron-muon aron-muon force-pushed the aron/oauth-improvements branch 2 times, most recently from fb61b6e to 831513f Compare January 28, 2026 18:30
@aron-muon aron-muon changed the title feat: introducing oauth 2.0 multi tenant support feat: OAuth 2.0 multi-user support with GovSlack compatibility Jan 28, 2026
@aron-muon aron-muon marked this pull request as ready for review January 28, 2026 20:25
@aron-muon aron-muon requested a review from korotovsky January 28, 2026 20:25
@aron-muon
Copy link
Contributor Author

OK, I've ran a significant amount of manual integration testing, and verified that the changes here work perfectly for handling Oauth for multiple users.

@korotovsky
Copy link
Owner

Hi @aron-muon, there was PR #141 merged in master, could you please rebase? After that over a weekend I will try to make a full review.

}

// channelsHandlerOAuth handles channel listing in OAuth mode
func (ch *ChannelsHandler) channelsHandlerOAuth(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
Copy link
Owner

Choose a reason for hiding this comment

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

Is there any way to avoid defining separate handlers for OAuth?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Introduced getEffectiveClient(ctx) which returns a *slack.Client regardless of mode, in OAuth it creates a per-user client from the request context, in legacy it returns the provider's shared client. This eliminated the repeated OAuth/legacy branching from 8 handlers (history, replies, search, add message, reactions add/remove, files get, and fetchSingleMessage).

The ChannelsHandler still has a separate OAuth path because legacy mode reads from a pre-built cache while OAuth fetches directly from the Slack API.

lswith added a commit to lswith/slack-mcp-server that referenced this pull request Feb 9, 2026
Add single-user local OAuth flow that starts a temporary HTTPS server
with a self-signed cert on localhost:8443, opens the browser for Slack
authorization, and exchanges the callback code for an xoxp token.

Token resolution in legacy mode now follows: env vars → file store
(~/.slack-mcp/token.json) → local OAuth flow, keeping full backward
compatibility while enabling interactive CLI login.

Also fixes two pre-existing compile errors in PR korotovsky#166's conversations
handler (missing parseParamsToolReaction args, duplicate hasMedia decl).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lswith
Copy link

lswith commented Feb 9, 2026

Hey! I've opened a stacked draft PR on top of this one that adds a local OAuth flow for single-user CLI usage: #195

It adds a self-signed cert HTTPS server on localhost:8443 that handles the browser callback, plus file-based token storage at ~/.slack-mcp/token.json so users dont need to re-auth each time.

@korotovsky
Copy link
Owner

Please rebase, I'm sorry for this.

@mrfelton
Copy link

Just curious if there is an ETA on this? It would make the slack mcp a lot more usable!

@aron-muon
Copy link
Contributor Author

Apologies ya'll - working through this now

@aron-muon aron-muon force-pushed the aron/oauth-improvements branch from 92aecd4 to d6338d1 Compare February 25, 2026 11:07
@aron-muon aron-muon requested a review from korotovsky February 25, 2026 11:31
@korotovsky
Copy link
Owner

Hi @aron-muon, please take a look here #195. Would it be possible to take the some progress back? That PR in draft and is not moving

wentaoyang-moloco and others added 8 commits February 26, 2026 12:45
Add OAuth 2.0 authentication flow for multi-user support with the following features:

Core OAuth Implementation:
- OAuth 2.0 authorization flow with Slack API
- CSRF protection via state parameter validation
- In-memory token storage with automatic cleanup
- Per-request Slack client creation for token isolation
- Support for both user tokens (xoxp) and bot tokens (xoxb)

Dual-Mode Architecture:
- Legacy mode: Single-user with browser tokens (xoxc/xoxd/xoxp)
- OAuth mode: Multi-user with OAuth tokens
- Backward compatible with existing configurations

OAuth Handlers:
- /oauth/authorize endpoint for initiating OAuth flow
- /oauth/callback endpoint for processing OAuth callbacks
- OAuth middleware for token validation and user context injection

Multi-User Features:
- Per-user token isolation and validation
- User context propagation through request chain
- Separate user and bot token support
- Option to post as bot or user via post_as_bot parameter

Security Enhancements:
- Secure state generation using crypto/rand
- Token validation on each request
- HTTPS requirement for OAuth callbacks (Slack requirement)
- Security headers on OAuth endpoints
- Credentials removed from example files

Documentation:
- Comprehensive OAuth setup guide (docs/04-oauth-setup.md)
- ngrok setup instructions for local development
- OAuth configuration examples
- Architecture and troubleshooting sections

Developer Tools:
- OAuth testing script (scripts/test-oauth.sh)
- OAuth server startup script (start-oauth-server.sh)
- Example configuration file (oauth.env.example)

Modified Components:
- main.go: Added OAuth mode detection and initialization
- channels.go: OAuth support with per-request client
- conversations.go: OAuth support with user/bot token selection
- server.go: OAuth-enabled SSE and HTTP server methods
- sse_auth.go: Exported WithAuthKey for OAuth middleware
- .gitignore: Added oauth.env and binary exclusions

New Components:
- pkg/oauth/: OAuth manager, storage, and types
- pkg/server/auth/: OAuth middleware and user context
- pkg/server/oauth_handler.go: OAuth HTTP handlers

Breaking Changes: None (fully backward compatible)

Environment Variables:
- SLACK_MCP_OAUTH_ENABLED: Enable OAuth mode
- SLACK_MCP_OAUTH_CLIENT_ID: Slack app client ID
- SLACK_MCP_OAUTH_CLIENT_SECRET: Slack app client secret
- SLACK_MCP_OAUTH_REDIRECT_URI: OAuth callback URL (requires HTTPS)
Skip TestIntegrationConversations as it requires:
- External Slack workspace with #testcase-1 channel and test data
- SLACK_MCP_OPENAI_API environment variable
- ngrok forwarding setup

This test is from the upstream repo and requires infrastructure
not available in CI. Test can be re-enabled when proper test
infrastructure is set up.
Remove ability to post as bot for security and clarity:
- Removed post_as_bot parameter from conversations handler
- Removed bot scopes from OAuth authorization request
- Simplified OAuth callback response (no bot token)
- Updated documentation to reflect user-only posting

Users will now always appear as themselves when posting messages,
never as the app bot. This provides better transparency and
prevents impersonation concerns.
Add comprehensive OAuth quick-start section to README:
- Step-by-step OAuth setup instructions
- Comparison with legacy mode (when to use each)
- OAuth environment variables added to reference table
- Clear benefits and use cases for OAuth vs legacy
- MCP client configuration examples

Makes it easier for users to understand and choose between
OAuth mode (multi-user, production) and legacy mode (single-user, testing).
- Add GovSlack token verification support
- Fix OAuth context URL handling and user info retrieval
- Update Docker Hub image name and credentials
- Fix member count and channel ID lookup issues
- Improve conversation and channel handling
- Add Trivy security scanning and Dependabot configuration
- Update dependencies and Docker entrypoint

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removed skip condition for integration test due to external dependencies.
… logic

Add getEffectiveClient helper method to ConversationsHandler that
abstracts Slack client retrieval for both OAuth and legacy modes.
This eliminates repeated conditional checks across multiple handlers
(ConversationsAddMessageHandler, ReactionsAddHandler, etc.) and
simplifies the codebase by removing duplicated OAuth/legacy branching
logic throughout the file.
@aron-muon aron-muon force-pushed the aron/oauth-improvements branch from b9239b0 to 94f8b15 Compare February 26, 2026 12:47
@aron-muon
Copy link
Contributor Author

Hi @aron-muon, please take a look here #195. Would it be possible to take the some progress back? That PR in draft and is not moving

No problem, I cleaned up the commit history - should be significantly easier to read now.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants