Skip to content

Commit 73e47af

Browse files
Merge pull request #16 from Stealinglight/fix-strava-mcp-auth-dWS4j
feat: Add authless mode for Claude.ai custom connectors Testing
2 parents af4c1e2 + f51d43a commit 73e47af

7 files changed

Lines changed: 300 additions & 41 deletions

File tree

AGENTS.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,20 +288,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
288288

289289
### Authentication Flow
290290

291-
**Two Types of Auth**:
291+
**Three Types of Auth**:
292292

293293
1. **Bearer Token** (AUTH_TOKEN):
294294
- Static token for Lambda authentication
295295
- Generated during deployment
296296
- Validates client requests to Lambda function
297297
- Stored in environment variable
298+
- Required for `/mcp` JSON-RPC endpoint
298299

299-
2. **OAuth Tokens** (Strava):
300+
2. **Authless Mode** (ALLOW_AUTHLESS):
301+
- When `ALLOW_AUTHLESS=true` (default), SSE endpoints bypass auth
302+
- Enables Claude.ai custom connectors (which don't support custom headers)
303+
- Applies to: `/sse`, `/sse/`, `/message` endpoints
304+
- Does NOT apply to: `/mcp` endpoint (still requires Bearer token)
305+
- Security: Keep Lambda URL private when enabled
306+
307+
3. **OAuth Tokens** (Strava):
300308
- Access token (short-lived, ~6 hours)
301309
- Refresh token (long-lived)
302310
- Automatically managed by StravaClient
303311
- Stored in environment variables (CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)
304312

313+
**Authentication Middleware Logic**:
314+
```typescript
315+
// Simplified auth flow
316+
if (path === '/health') → bypass auth
317+
if (ALLOW_AUTHLESS && (path === '/sse' || path === '/message')) → bypass auth
318+
if (path === '/message' && valid session) → bypass auth
319+
elserequire Bearer token
320+
```
321+
305322
### Local Development Workflow
306323

307324
```bash
@@ -539,6 +556,7 @@ sam delete --stack-name strava-mcp-stack # Delete deployment
539556
- `STRAVA_CLIENT_SECRET` - Strava API client secret
540557
- `STRAVA_REFRESH_TOKEN` - Strava OAuth refresh token
541558
- `AUTH_TOKEN` - Bearer token for Lambda authentication
559+
- `ALLOW_AUTHLESS` - When "true", SSE endpoints bypass auth (default: "true")
542560
- `NODE_ENV` - Environment (production/development)
543561

544562
### File Locations

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,32 @@ Need to see the config again? Run:
134134
bun run deploy:show-config
135135
```
136136

137+
### 5. Claude.ai Custom Connectors (Web/Mobile)
138+
139+
Claude.ai custom connectors use **authless mode** via SSE transport. This is enabled by default (`ALLOW_AUTHLESS=true`).
140+
141+
**Setup Steps:**
142+
1. Go to **Claude.ai****Settings****Connectors****Add custom connector**
143+
2. Enter just the **base URL** (no path): `https://your-function-url.lambda-url.us-east-1.on.aws`
144+
3. Claude.ai will automatically discover the `/sse` endpoint and connect
145+
4. Verify tools appear and can be invoked
146+
147+
**Important Notes:**
148+
- Claude.ai does NOT support custom headers (no Bearer token auth)
149+
- Authless mode only applies to SSE endpoints (`/sse`, `/message`)
150+
- The `/mcp` JSON-RPC endpoint still requires Bearer token authentication
151+
- Keep your Lambda Function URL private for security
152+
153+
**Security Considerations:**
154+
- Authless mode is **high risk** and should be treated as **personal-use-only**
155+
- If authless mode is enabled, anyone with your Lambda URL can access and act on your Strava data
156+
- Authless mode is **not recommended for production**; if you must use it, combine it with stricter controls such as AWS WAF IP allowlisting or an additional shared secret
157+
158+
**Recommended secure deployment (authless disabled, Bearer tokens required everywhere):**
159+
```bash
160+
sam deploy --parameter-overrides AllowAuthless=false
161+
```
162+
137163
## Documentation
138164

139165
📚 **[Full Documentation](https://stealinglight.github.io/StravaMCP)**

docs/authentication.md

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ The Lambda function uses **Bearer token authentication** to secure access to you
1111
- ✅ Works with Claude web, mobile, and desktop
1212
- ✅ Easy to rotate tokens when needed
1313

14-
## How It Works
14+
## Authentication Modes
15+
16+
The server supports two authentication modes:
17+
18+
### 1. Bearer Token Authentication (Default for `/mcp` endpoint)
19+
20+
For Claude Desktop and other MCP clients that support custom headers.
1521

1622
```
1723
┌──────────────┐
@@ -31,6 +37,33 @@ The Lambda function uses **Bearer token authentication** to secure access to you
3137
└──────────────┘
3238
```
3339

40+
### 2. Authless Mode (For Claude.ai Custom Connectors)
41+
42+
Claude.ai custom connectors only support authless or OAuth 2.1 (DCR) connections. Since OAuth 2.1 with DCR is complex to implement, this server provides an **authless mode** for SSE endpoints.
43+
44+
When `ALLOW_AUTHLESS=true` (default), the SSE transport endpoints (`/sse`, `/sse/`, `/message`) bypass authentication, allowing Claude.ai to connect without Bearer tokens.
45+
46+
```
47+
┌──────────────┐
48+
│ Claude.ai │
49+
│ (Web/Mobile) │
50+
└──────┬───────┘
51+
│ SSE connection (no auth required)
52+
53+
┌──────────────┐
54+
│ Lambda │─── ALLOW_AUTHLESS=true
55+
│ (Middleware)│ Skips auth for SSE
56+
└──────┬───────┘
57+
│ Direct access
58+
59+
┌──────────────┐
60+
│ MCP Server │
61+
│ (Strava API) │
62+
└──────────────┘
63+
```
64+
65+
**Important**: The `/mcp` JSON-RPC endpoint still requires Bearer token authentication for backward compatibility with other clients.
66+
3467
## Setup Steps
3568

3669
### 1. Deploy and Auto-Generate AUTH_TOKEN
@@ -116,6 +149,20 @@ Add as a Custom Connector in Claude Settings:
116149

117150
**Important**: The URL must end with `/mcp` - this is the MCP endpoint.
118151

152+
#### Claude.ai Custom Connectors (Authless Mode)
153+
154+
For Claude.ai web and mobile, use the authless SSE transport:
155+
156+
1. Go to **Claude.ai****Settings****Connectors****Add custom connector**
157+
2. Enter just the **base URL** (no `/mcp` or `/sse` path):
158+
```
159+
https://your-function-url.lambda-url.us-east-1.on.aws
160+
```
161+
3. Claude.ai will automatically discover the `/sse` endpoint
162+
4. Verify tools appear in the conversation
163+
164+
**Note**: Authless mode is enabled by default (`ALLOW_AUTHLESS=true`). Claude.ai cannot send custom headers, so Bearer token auth is not supported for Claude.ai connectors.
165+
119166
### 4. Test Authentication
120167

121168
Test that authentication is working:
@@ -171,6 +218,51 @@ To rotate your authentication token:
171218
- Use short or predictable tokens
172219
- Reuse tokens across multiple services
173220

221+
## Authless Mode Configuration
222+
223+
### Enabling Authless Mode (Default)
224+
225+
Authless mode is **enabled by default** for Claude.ai compatibility. The `ALLOW_AUTHLESS` parameter is set to `"true"` in the SAM template.
226+
227+
When enabled:
228+
- SSE endpoints (`/sse`, `/sse/`, `/message`) do not require authentication
229+
- The `/mcp` JSON-RPC endpoint still requires Bearer token authentication
230+
- Claude.ai custom connectors can connect using the base URL
231+
232+
### Disabling Authless Mode
233+
234+
To require Bearer token authentication for all endpoints:
235+
236+
```bash
237+
# During deployment
238+
sam deploy --parameter-overrides AllowAuthless=false
239+
240+
# Or update samconfig.toml
241+
[default.deploy.parameters]
242+
parameter_overrides = [
243+
"AllowAuthless=false",
244+
# ... other parameters
245+
]
246+
```
247+
248+
When disabled:
249+
- All endpoints require Bearer token authentication
250+
- Claude.ai custom connectors will NOT work (they cannot send custom headers)
251+
- Only Claude Desktop and clients supporting custom headers can connect
252+
253+
### Security Implications
254+
255+
**Authless mode enabled (`ALLOW_AUTHLESS=true`, advanced / opt-in):**
256+
- ⚠️ Not enabled by default. Enabling this on a publicly accessible Lambda URL exposes `/sse`, `/sse/`, and `/message` without authentication.
257+
- ⚠️ Anyone who learns your Lambda URL (via logs, screenshots, or misconfiguration) can access your Strava-backed tools via SSE; URL secrecy alone is **not** sufficient protection.
258+
- ✅ Only enable for strictly local or personal use where you fully control access to the URL.
259+
- 💡 If you must use authless mode in a cloud environment, combine it with additional controls such as AWS WAF/IP allowlisting and/or an extra shared secret (e.g., a query parameter or header checked by your Lambda).
260+
261+
**Authless mode disabled (`ALLOW_AUTHLESS=false`, recommended default):**
262+
- ✅ All requests require a valid Bearer token
263+
- ❌ Claude.ai custom connectors won't work
264+
- ✅ Maximum security for shared, team, or production environments
265+
174266
## Security Considerations
175267

176268
### What This Protects

src/config/env.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const envSchema = z.object({
1212
STRAVA_REFRESH_TOKEN: z.string().min(1, 'STRAVA_REFRESH_TOKEN is required'),
1313
AUTH_TOKEN: z.string().min(32, 'AUTH_TOKEN must be at least 32 characters'),
1414
PORT: z.string().default('3000').transform(Number),
15+
// ALLOW_AUTHLESS: When "true", bypasses auth for SSE endpoints (/sse, /sse/, /message)
16+
// This enables Claude.ai custom connectors which don't support Bearer token auth
17+
ALLOW_AUTHLESS: z.string().default('true').transform((val) => val.toLowerCase() === 'true'),
1518
});
1619

1720
/**
@@ -25,6 +28,7 @@ export function getConfig() {
2528
STRAVA_REFRESH_TOKEN: process.env.STRAVA_REFRESH_TOKEN,
2629
AUTH_TOKEN: process.env.AUTH_TOKEN,
2730
PORT: process.env.PORT,
31+
ALLOW_AUTHLESS: process.env.ALLOW_AUTHLESS,
2832
});
2933

3034
if (!result.success) {

src/index.ts

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ try {
9292
const server = new Server(
9393
{
9494
name: 'strava-mcp-server',
95-
version: '1.0.0',
95+
version: '3.0.0',
9696
},
9797
{
9898
capabilities: {
@@ -325,50 +325,91 @@ async function main() {
325325

326326
// Authentication middleware for local dev (optional - can be disabled)
327327
// Supports both Authorization header and query parameter
328+
// When ALLOW_AUTHLESS=true, SSE endpoints bypass auth for Claude.ai custom connectors
328329
app.use((req: Request, res: Response, next) => {
329-
if (req.path === '/health') {
330+
if (req.path === '/health' || req.path === '/debug') {
330331
return next();
331332
}
332333

333-
// For SSE endpoint, authentication happens via query param or header
334-
if (req.path === '/sse' || req.path === '/sse/') {
335-
// SSE authentication happens here before establishing connection
336-
// Continue to token validation below
334+
// Check if authless mode is enabled for SSE endpoints
335+
// This allows Claude.ai custom connectors to connect without Bearer tokens
336+
const isSSEEndpoint = req.path === '/sse' || req.path === '/sse/';
337+
const isMessageEndpoint = req.path === '/message';
338+
339+
if (config.ALLOW_AUTHLESS && (isSSEEndpoint || isMessageEndpoint)) {
340+
if (isSSEEndpoint) {
341+
console.error('[StravaServer] Authless SSE connection allowed (ALLOW_AUTHLESS=true)');
342+
}
343+
return next();
337344
}
338345

339-
// For SSE message endpoint, trust valid session IDs
346+
// For SSE message endpoint with valid session, trust the session
340347
const sessionId = req.query.sessionId as string;
341-
if (req.path === '/message' && sessionId && transports[sessionId]) {
348+
if (isMessageEndpoint && sessionId && transports[sessionId]) {
342349
return next();
343350
}
344351

345352
// For local development, authentication is optional
346353
// If AUTH_TOKEN is set in .env, validate it
347354
if (config.AUTH_TOKEN && config.AUTH_TOKEN.length > 0) {
348355
let token: string | undefined;
349-
356+
350357
const authHeader = req.headers['authorization'];
351358
if (authHeader && authHeader.startsWith('Bearer ')) {
352359
token = authHeader.substring(7);
353360
} else if (req.query.token) {
354361
token = req.query.token as string;
355362
}
356-
363+
357364
if (!token || token !== config.AUTH_TOKEN) {
358365
console.error('[StravaServer] Invalid or missing token');
359-
return res.status(401).json({
366+
return res.status(401).json({
360367
error: 'Unauthorized',
361368
message: 'Invalid or missing token'
362369
});
363370
}
364371
}
365-
372+
366373
next();
367374
});
368375

369-
// Health check endpoint
376+
// Health check endpoint - enhanced with diagnostic info
370377
app.get('/health', (_req: Request, res: Response) => {
371-
res.json({ status: 'healthy', version: '2.0.0' });
378+
res.json({
379+
status: 'healthy',
380+
version: '3.0.0',
381+
runtime: 'local',
382+
authless: config?.ALLOW_AUTHLESS ?? false,
383+
timestamp: new Date().toISOString(),
384+
});
385+
});
386+
387+
// Debug endpoint - helps troubleshoot Claude.ai connection issues
388+
app.get('/debug', (_req: Request, res: Response) => {
389+
res.json({
390+
status: 'ok',
391+
version: '3.0.0',
392+
authless_enabled: config?.ALLOW_AUTHLESS ?? false,
393+
environment: process.env.NODE_ENV || 'development',
394+
endpoints: {
395+
health: '/health',
396+
debug: '/debug',
397+
sse: '/sse (GET, establishes SSE connection)',
398+
message: '/message (POST, requires sessionId query param)',
399+
mcp: '/mcp (POST, requires Bearer token)',
400+
},
401+
claude_ai_setup: {
402+
connector_url: `Use base URL only: http://localhost:${config.PORT}`,
403+
auth_mode: config?.ALLOW_AUTHLESS ? 'Authless (SSE endpoints bypass auth)' : 'Bearer token required',
404+
transport: 'SSE',
405+
note: 'For local testing with Claude.ai, use ngrok or similar to expose localhost',
406+
},
407+
sse_headers: {
408+
'Content-Type': 'text/event-stream',
409+
'Cache-Control': 'no-cache',
410+
Connection: 'keep-alive',
411+
},
412+
});
372413
});
373414

374415
// SSE endpoint - establishes the server-to-client event stream
@@ -453,7 +494,7 @@ async function main() {
453494
},
454495
serverInfo: {
455496
name: 'strava-mcp-server',
456-
version: '2.0.0',
497+
version: '3.0.0',
457498
},
458499
};
459500
break;
@@ -674,7 +715,10 @@ async function main() {
674715
app.listen(port, () => {
675716
console.error(`[StravaServer] Remote MCP server running on http://localhost:${port}`);
676717
console.error(`[StravaServer] MCP endpoint: http://localhost:${port}/mcp`);
718+
console.error(`[StravaServer] SSE endpoint: http://localhost:${port}/sse`);
677719
console.error(`[StravaServer] Health check: http://localhost:${port}/health`);
720+
console.error(`[StravaServer] Debug info: http://localhost:${port}/debug`);
721+
console.error(`[StravaServer] Authless mode: ${config.ALLOW_AUTHLESS ? 'ENABLED' : 'DISABLED'}`);
678722
});
679723
}
680724

0 commit comments

Comments
 (0)