Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,20 +288,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({

### Authentication Flow

**Two Types of Auth**:
**Three Types of Auth**:

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

2. **OAuth Tokens** (Strava):
2. **Authless Mode** (ALLOW_AUTHLESS):
- When `ALLOW_AUTHLESS=true` (default), SSE endpoints bypass auth
- Enables Claude.ai custom connectors (which don't support custom headers)
- Applies to: `/sse`, `/sse/`, `/message` endpoints
- Does NOT apply to: `/mcp` endpoint (still requires Bearer token)
- Security: Keep Lambda URL private when enabled

3. **OAuth Tokens** (Strava):
- Access token (short-lived, ~6 hours)
- Refresh token (long-lived)
- Automatically managed by StravaClient
- Stored in environment variables (CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)

**Authentication Middleware Logic**:
```typescript
// Simplified auth flow
if (path === '/health') → bypass auth
if (ALLOW_AUTHLESS && (path === '/sse' || path === '/message')) → bypass auth
if (path === '/message' && valid session) → bypass auth
else → require Bearer token
```
Comment thread
Stealinglight marked this conversation as resolved.

### Local Development Workflow

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

### File Locations
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,32 @@ Need to see the config again? Run:
bun run deploy:show-config
```

### 5. Claude.ai Custom Connectors (Web/Mobile)

Claude.ai custom connectors use **authless mode** via SSE transport. This is enabled by default (`ALLOW_AUTHLESS=true`).

**Setup Steps:**
1. Go to **Claude.ai** → **Settings** → **Connectors** → **Add custom connector**
2. Enter just the **base URL** (no path): `https://your-function-url.lambda-url.us-east-1.on.aws`
3. Claude.ai will automatically discover the `/sse` endpoint and connect
4. Verify tools appear and can be invoked

**Important Notes:**
- Claude.ai does NOT support custom headers (no Bearer token auth)
- Authless mode only applies to SSE endpoints (`/sse`, `/message`)
- The `/mcp` JSON-RPC endpoint still requires Bearer token authentication
- Keep your Lambda Function URL private for security

**Security Considerations:**
- Anyone with your Lambda URL can access your Strava data in authless mode
- This is acceptable for personal use where the URL is not shared
- For production, consider adding IP allowlisting via AWS WAF

To disable authless mode (require Bearer tokens everywhere):
Comment thread
Stealinglight marked this conversation as resolved.
Outdated
```bash
sam deploy --parameter-overrides AllowAuthless=false
```

## Documentation

📚 **[Full Documentation](https://stealinglight.github.io/StravaMCP)**
Expand Down
94 changes: 93 additions & 1 deletion docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ The Lambda function uses **Bearer token authentication** to secure access to you
- ✅ Works with Claude web, mobile, and desktop
- ✅ Easy to rotate tokens when needed

## How It Works
## Authentication Modes

The server supports two authentication modes:

### 1. Bearer Token Authentication (Default for `/mcp` endpoint)

For Claude Desktop and other MCP clients that support custom headers.

```
┌──────────────┐
Expand All @@ -31,6 +37,33 @@ The Lambda function uses **Bearer token authentication** to secure access to you
└──────────────┘
```

### 2. Authless Mode (For Claude.ai Custom Connectors)

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.

When `ALLOW_AUTHLESS=true` (default), the SSE transport endpoints (`/sse`, `/sse/`, `/message`) bypass authentication, allowing Claude.ai to connect without Bearer tokens.

```
┌──────────────┐
│ Claude.ai │
│ (Web/Mobile) │
└──────┬───────┘
│ SSE connection (no auth required)
┌──────────────┐
│ Lambda │─── ALLOW_AUTHLESS=true
│ (Middleware)│ Skips auth for SSE
└──────┬───────┘
│ Direct access
┌──────────────┐
│ MCP Server │
│ (Strava API) │
└──────────────┘
```

**Important**: The `/mcp` JSON-RPC endpoint still requires Bearer token authentication for backward compatibility with other clients.

## Setup Steps

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

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

#### Claude.ai Custom Connectors (Authless Mode)

For Claude.ai web and mobile, use the authless SSE transport:

1. Go to **Claude.ai** → **Settings** → **Connectors** → **Add custom connector**
2. Enter just the **base URL** (no `/mcp` or `/sse` path):
```
https://your-function-url.lambda-url.us-east-1.on.aws
```
3. Claude.ai will automatically discover the `/sse` endpoint
4. Verify tools appear in the conversation

**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.

### 4. Test Authentication

Test that authentication is working:
Expand Down Expand Up @@ -171,6 +218,51 @@ To rotate your authentication token:
- Use short or predictable tokens
- Reuse tokens across multiple services

## Authless Mode Configuration

### Enabling Authless Mode (Default)

Authless mode is **enabled by default** for Claude.ai compatibility. The `ALLOW_AUTHLESS` parameter is set to `"true"` in the SAM template.

When enabled:
- SSE endpoints (`/sse`, `/sse/`, `/message`) do not require authentication
- The `/mcp` JSON-RPC endpoint still requires Bearer token authentication
- Claude.ai custom connectors can connect using the base URL

### Disabling Authless Mode

To require Bearer token authentication for all endpoints:

```bash
# During deployment
sam deploy --parameter-overrides AllowAuthless=false

# Or update samconfig.toml
[default.deploy.parameters]
parameter_overrides = [
"AllowAuthless=false",
# ... other parameters
]
```

When disabled:
- All endpoints require Bearer token authentication
- Claude.ai custom connectors will NOT work (they cannot send custom headers)
- Only Claude Desktop and clients supporting custom headers can connect

### Security Implications

**Authless mode enabled (`ALLOW_AUTHLESS=true`):**
- ⚠️ Anyone with your Lambda URL can access your Strava data via SSE
- ✅ Keep your Lambda Function URL private
- ✅ Acceptable for personal use where URL is not shared
- 💡 Consider AWS WAF for IP allowlisting in production

**Authless mode disabled (`ALLOW_AUTHLESS=false`):**
- ✅ All requests require valid Bearer token
- ❌ Claude.ai custom connectors won't work
- ✅ Maximum security for shared environments
Comment thread
Stealinglight marked this conversation as resolved.
Outdated

## Security Considerations

### What This Protects
Expand Down
4 changes: 4 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const envSchema = z.object({
STRAVA_REFRESH_TOKEN: z.string().min(1, 'STRAVA_REFRESH_TOKEN is required'),
AUTH_TOKEN: z.string().min(32, 'AUTH_TOKEN must be at least 32 characters'),
PORT: z.string().default('3000').transform(Number),
// ALLOW_AUTHLESS: When "true", bypasses auth for SSE endpoints (/sse, /sse/, /message)
// This enables Claude.ai custom connectors which don't support Bearer token auth
ALLOW_AUTHLESS: z.string().default('false').transform((val) => val.toLowerCase() === 'true'),
Comment thread
Stealinglight marked this conversation as resolved.
Outdated
});

/**
Expand All @@ -25,6 +28,7 @@ export function getConfig() {
STRAVA_REFRESH_TOKEN: process.env.STRAVA_REFRESH_TOKEN,
AUTH_TOKEN: process.env.AUTH_TOKEN,
PORT: process.env.PORT,
ALLOW_AUTHLESS: process.env.ALLOW_AUTHLESS,
});

if (!result.success) {
Expand Down
27 changes: 17 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,44 +325,51 @@ async function main() {

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

// For SSE endpoint, authentication happens via query param or header
if (req.path === '/sse' || req.path === '/sse/') {
// SSE authentication happens here before establishing connection
// Continue to token validation below
// Check if authless mode is enabled for SSE endpoints
// This allows Claude.ai custom connectors to connect without Bearer tokens
const isSSEEndpoint = req.path === '/sse' || req.path === '/sse/';
const isMessageEndpoint = req.path === '/message';

if (config.ALLOW_AUTHLESS && (isSSEEndpoint || isMessageEndpoint)) {
if (isSSEEndpoint) {
console.error('[StravaServer] Authless SSE connection allowed (ALLOW_AUTHLESS=true)');
}
return next();
}

// For SSE message endpoint, trust valid session IDs
// For SSE message endpoint with valid session, trust the session
const sessionId = req.query.sessionId as string;
if (req.path === '/message' && sessionId && transports[sessionId]) {
if (isMessageEndpoint && sessionId && transports[sessionId]) {
return next();
}

// For local development, authentication is optional
// If AUTH_TOKEN is set in .env, validate it
if (config.AUTH_TOKEN && config.AUTH_TOKEN.length > 0) {
let token: string | undefined;

const authHeader = req.headers['authorization'];
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7);
} else if (req.query.token) {
token = req.query.token as string;
}

if (!token || token !== config.AUTH_TOKEN) {
console.error('[StravaServer] Invalid or missing token');
return res.status(401).json({
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid or missing token'
});
}
}

next();
});

Expand Down
33 changes: 20 additions & 13 deletions src/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,25 +91,37 @@ function initializeServer(): express.Application {
// Authentication middleware - verify Bearer token
// Skip auth for health check endpoint
// Supports both Authorization header and query parameter for Claude connector compatibility
// When ALLOW_AUTHLESS=true, SSE endpoints bypass auth for Claude.ai custom connectors
app.use((req: Request, res: Response, next) => {
if (req.path === '/health') {
return next();
}

// For SSE endpoint, authentication happens via query param or header
if (req.path === '/sse' || req.path === '/sse/') {
// SSE authentication happens here before establishing connection
// Continue to token validation below
if (!config) {
// Initialize config if not already done (shouldn't happen, but safety check)
config = getConfig();
}

// For SSE message endpoint, trust valid session IDs
// Check if authless mode is enabled for SSE endpoints
// This allows Claude.ai custom connectors to connect without Bearer tokens
const isSSEEndpoint = req.path === '/sse' || req.path === '/sse/';
const isMessageEndpoint = req.path === '/message';

if (config.ALLOW_AUTHLESS && (isSSEEndpoint || isMessageEndpoint)) {
if (isSSEEndpoint) {
console.error('[StravaLambda] Authless SSE connection allowed (ALLOW_AUTHLESS=true)');
}
return next();
}

// For SSE message endpoint with valid session, trust the session
const sessionId = req.query.sessionId as string;
if (req.path === '/message' && sessionId && transports[sessionId]) {
if (isMessageEndpoint && sessionId && transports[sessionId]) {
return next();
}

let token: string | undefined;

// Try Authorization header first (preferred for API clients)
const authHeader = req.headers['authorization'];
if (authHeader && authHeader.startsWith('Bearer ')) {
Expand All @@ -119,15 +131,10 @@ function initializeServer(): express.Application {
else if (req.query.token) {
token = req.query.token as string;
}

if (!config) {
// Initialize config if not already done (shouldn't happen, but safety check)
config = getConfig();
}

if (!token || token !== config.AUTH_TOKEN) {
console.error('[StravaLambda] Invalid or missing token');
return res.status(401).json({
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid or missing token. Use: Authorization: Bearer <token> OR ?token=<token>'
});
Expand Down
9 changes: 9 additions & 0 deletions template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ Parameters:
NoEcho: true
MinLength: 32

AllowAuthless:
Type: String
Description: When "true", allows SSE connections without authentication (for Claude.ai custom connectors)
Default: "true"
AllowedValues:
- "true"
- "false"

Resources:
StravaMCPFunction:
Type: AWS::Serverless::Function
Expand All @@ -53,6 +61,7 @@ Resources:
STRAVA_CLIENT_SECRET: !Ref StravaClientSecret
STRAVA_REFRESH_TOKEN: !Ref StravaRefreshToken
AUTH_TOKEN: !Ref AuthToken
ALLOW_AUTHLESS: !Ref AllowAuthless
FunctionUrlConfig:
AuthType: NONE # Public URL with Bearer token authentication in application code
Cors:
Expand Down
Loading