This document covers the OAuth BDD testing infrastructure for muster, including architecture, test scenarios, debugging tips, and implementation guidance.
The OAuth testing infrastructure enables comprehensive testing of muster's authentication implementation:
- ADR-004: OAuth Proxy (Muster Server β Remote MCP Servers)
- ADR-005: Muster Server Auth (Agent β Muster Server)
- ADR-008: Unified Authentication (auth status polling,
_metafields, SSO detection)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BDD Test Environment β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββββββββ β
β β Test Runner ββββββΆβ Muster ββββββΆβ Protected Mock MCP Server β β
β β + MCP Clientβ β Serve β β (validates against OAuth) β β
β ββββββββββββββββ β (separate β βββββββββββββββββ¬βββββββββββββββ β
β β β process) β β β
β β ββββββββ¬ββββββββ β β
β β β β β
β β βΌ βΌ β
β β ββββββββββββββββββββββββββββββββββββββββ β
β ββββββββββββββΆβ Mock OAuth Server β β
β β (validates tokens for protected MCP) β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β Layer 1: Cursor/IDE β Agent (stdio) β
β - No authentication required β
β - Agent exposes MCP tools to IDE β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Layer 2: Agent β Muster Server (HTTP/SSE) [ADR-005] β
β - Google OAuth (or Dex) protects muster server endpoints β
β - Agent detects 401, creates synthetic `authenticate_muster` tool β
β - Local callback server on port 3000 β
β - Token stored: ~/.config/muster/tokens/ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Layer 3: Muster Server β Remote MCP Servers (OAuth Proxy) [ADR-004] β
β - Server acts as OAuth client for remote MCPs β
β - Intercepts 401 from remote MCPs β
β - Exposes `authenticate_<server>` tools β
β - Token stored: Valkey (server-side, per session) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Cross-Cutting: ADR-008 Unified Authentication β
β - Agent polls auth://status every 30s β
β - Every tool response includes _meta["giantswarm.io/auth_required"] β
β - SSO detection via issuer grouping β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
There are TWO token stores in the test environment:
| Store | Location | Purpose | Accessible By |
|---|---|---|---|
| Mock OAuth Server | mock.OAuthServer.issuedTokens |
Validate tokens for protected MCP servers | Mock infrastructure |
| Muster Token Store | oauth.TokenStore.tokens |
Store tokens for aggregator to use | Muster process only |
The test_simulate_oauth_callback tool bridges these by completing the full OAuth flow, which stores the token in BOTH locations.
1. Test calls protected tool β Aggregator proxies to protected MCP server
2. Protected MCP returns 401 β Aggregator marks server as auth_required
3. Aggregator exposes authenticate_X β Synthetic tool appears in tool list
4. Test calls authenticate_X β Muster calls CreateAuthChallenge()
- Generates PKCE verifier
- Stores state in StateStore
- Returns auth URL with real state
5. Test parses auth URL β Extracts state, redirect_uri, etc.
6. Test generates auth code β Mock OAuth server stores code
7. Test calls muster callback β GET /callback?code=XXX&state=YYY
8. Muster validates state β Finds it in StateStore β
9. Muster exchanges code β POST to mock OAuth /token endpoint
10. Mock OAuth returns token β Muster stores in TokenStore
11. Test retries protected tool β Aggregator finds token, sends it
12. Protected MCP validates token β Against mock OAuth server β
13. Tool executes successfully β Protected tools now available
| Component | Location | Purpose |
|---|---|---|
| Mock OAuth Server | internal/testing/mock/oauth_server.go |
OAuth 2.1 server for testing |
| Mock Clock | internal/testing/mock/clock.go |
Time manipulation for expiry tests |
| Protected MCP Server | internal/testing/mock/protected_mcp_server.go |
MCP server with OAuth protection |
| Test Fixtures | internal/testing/fixtures/oauth/ |
Sample tokens and metadata |
test_simulate_oauth_callback |
internal/testing/test_tools.go |
Complete OAuth flow simulation |
test_inject_token |
internal/testing/test_tools.go |
Direct token injection |
test_get_oauth_server_info |
internal/testing/test_tools.go |
OAuth server state inspection |
| Scenario | Description | Status |
|---|---|---|
oauth-auth-meta-propagation |
Auth status in responses | β Passing |
oauth-full-stack |
End-to-end with protected MCPs | β Passing |
oauth-mock-server-basic |
Basic mock OAuth functionality | β Passing |
oauth-protected-mcp-server |
Protected MCP with simulated auth | β Passing |
oauth-sso-detection |
SSO hint for same-issuer servers | β Passing |
oauth-sso-shared-issuer |
SSO across servers with same issuer | β Passing |
oauth-token-injection |
Direct token injection testing | β Passing |
oauth-token-refresh-flow |
Token refresh behavior | β Passing |
The aggregator maintains per-session views of available tools. Each session only sees tools from OAuth-protected servers they've authenticated with.
Key code path:
- Client calls
tools/listvia MCP sessionToolFilter()intercepts the requestGetAllToolsForSession()computes session-specific tools- For OAuth servers: check connection status
- Return session's tools OR synthetic auth tool based on auth state
The aggregator's SSO mechanism uses issuer-based token lookup:
token := oauthHandler.GetTokenByIssuer(sessionID, authInfo.Issuer)SSO works because:
- Server A and Server B both use issuer
https://idp.example.com - User authenticates to Server A β token stored under
(sessionID, issuer) - User tries Server B β aggregator calls
GetTokenByIssuer(sessionID, issuer) - Finds existing token β SSO works!
StatusConnected- normal, fully connected serversStatusDisconnected- connection lostStatusAuthRequired- OAuth required, not yet authenticated
| Tool | Injects Into | Use Case |
|---|---|---|
test_inject_token |
Mock OAuth Server only | Testing token validation, 401 behavior |
test_simulate_oauth_callback |
Both (via full flow) | Testing complete OAuth integration |
test_get_oauth_server_info |
N/A (read-only) | Debugging OAuth server state |
test_inject_token:
- Testing mock OAuth server's token validation
- Testing protected MCP server's 401/200 behavior
- NOT for testing the full OAuth flow through muster
test_simulate_oauth_callback:
- Testing the complete OAuth integration
- Verifying SSO (Token Forwarding, Token Exchange)
- Any scenario where muster needs to use the token
name: "oauth-protected-mcp-server"
category: "behavioral"
concept: "mcpserver"
tags: ["oauth", "authentication"]
timeout: "2m"
pre_configuration:
mock_oauth_servers:
- name: "mock-idp"
scopes: ["openid", "profile", "mcp:read"]
auto_approve: true
pkce_required: true
token_lifetime: "1h"
mcp_servers:
- name: "protected-server"
config:
type: "streamable-http"
oauth:
required: true
mock_oauth_server_ref: "mock-idp"
scope: "mcp:read"
tools:
- name: "get_secret"
responses:
- response:
secret: "super-secret-value"
steps:
- id: verify-auth-required
tool: "x_protected-server_get_secret"
args: {}
expected:
success: false
error_contains: ["authentication required"]
- id: authenticate
tool: "core_auth_login"
args:
server: "protected-server"
expected:
success: true
contains: ["http"]
- id: complete-oauth-flow
tool: "test_simulate_oauth_callback"
args:
server: "protected-server"
expected:
success: true
- id: call-protected-tool
tool: "x_protected-server_get_secret"
args: {}
expected:
success: true
contains: ["super-secret-value"]name: "oauth-sso-detection"
category: "behavioral"
concept: "mcpserver"
tags: ["oauth", "sso"]
timeout: "2m"
pre_configuration:
mock_oauth_servers:
- name: "shared-idp"
scopes: ["openid", "profile", "mcp:admin"]
auto_approve: true
mcp_servers:
- name: "sso-server-a"
config:
type: "streamable-http"
oauth:
required: true
mock_oauth_server_ref: "shared-idp"
tools:
- name: "op_a"
responses:
- response: { source: "server-a" }
- name: "sso-server-b"
config:
type: "streamable-http"
oauth:
required: true
mock_oauth_server_ref: "shared-idp" # Same issuer
tools:
- name: "op_b"
responses:
- response: { source: "server-b" }
steps:
- id: authenticate-server-a
tool: "test_simulate_oauth_callback"
args:
server: "sso-server-a"
expected:
success: true
- id: call-server-a-tool
tool: "x_sso-server-a_op_a"
args: {}
expected:
success: true
contains: ["server-a"]
# SSO should work for second server
- id: sso-authenticate-server-b
tool: "core_auth_login"
args:
server: "sso-server-b"
expected:
success: true
contains: ["Successfully connected"]
- id: call-server-b-tool
tool: "x_sso-server-b_op_b"
args: {}
expected:
success: true
contains: ["server-b"]muster test --scenario oauth-protected-mcp-server --verbose --debugSymptom: Calling a protected tool returns "tool not found" instead of an authentication error.
Cause: The server might not be properly registered in StatusAuthRequired state.
Debug steps:
- Check if the mock OAuth server is running (
test_get_oauth_server_info) - Verify
/.well-known/oauth-protected-resourceis accessible - Verify the server appears in
core_mcpserver_listoutput - Enable debug logging
Symptom: test_simulate_oauth_callback succeeds but protected tools fail.
Cause: Token is in mock OAuth server but not in muster's token store.
Debug steps:
- Check if callback URL was correct
- Verify the state parameter matches
- Check muster logs for callback handling errors
Symptom: Have to authenticate separately to each server even with same issuer.
Cause: Servers might be using different mock OAuth servers.
Debug steps:
- Verify both servers reference the SAME
mock_oauth_server_ref - Check that the issuer URL is identical
- Confirm the first authentication completed fully
Symptom: Token exists but GetTokenByIssuer returns nil.
Cause: Different session IDs between callback and tool call.
Debug steps:
- Check if session ID is propagated correctly
- Look for
sessionIDin debug logs
# Run with full debug output
muster test --scenario oauth-protected-mcp-server --verbose --debug 2>&1 | tee /tmp/oauth-debug.log
# Search for specific events
grep "Session" /tmp/oauth-debug.log
grep "GetTokenByIssuer" /tmp/oauth-debug.log
grep "callback" /tmp/oauth-debug.log# Rebuild after code changes
go install
# Run specific OAuth scenario
muster test --scenario oauth-protected-mcp-server --verbose
# Run all OAuth scenarios
muster test --scenario oauth-auth-meta-propagation --verbose
muster test --scenario oauth-full-stack --verbose
muster test --scenario oauth-mock-server-basic --verbose
muster test --scenario oauth-protected-mcp-server --verbose
muster test --scenario oauth-sso-detection --verbose
muster test --scenario oauth-sso-shared-issuer --verbose
muster test --scenario oauth-token-injection --verbose
muster test --scenario oauth-token-refresh-flow --verbose
# Full test suite
make test
muster test --parallel 50Architecture diagrams are available in the diagrams/ subdirectory:
oauth-test-setup.png- Overview diagramoauth-test-setup-system-view.png- System contextoauth-test-setup-container-view.png- Container architectureoauth-test-setup-components-view.png- Component detailsoauth-test-setup-sequence.png- Sequence diagramoauth-test-setup-code-view.png- Code-level view