Skip to content

Conversation

@crystalin
Copy link
Contributor

@crystalin crystalin commented Nov 4, 2025

Summary

Implements user credential passthrough mode, allowing projects to accept user-provided Anthropic credentials instead of requiring organization-managed accounts. Users can now pass their own Authorization: Bearer <token> headers, which are forwarded directly to Anthropic API with full header preservation.

Key Features

1. User Passthrough Mode

  • Projects with default_account_id = null accept user-provided Bearer tokens
  • User credentials forwarded directly to Anthropic API
  • Billing goes to user's personal Anthropic account
  • Dashboard UI supports "👤 User Account" selection

2. Authentication Priority (Updated)

  1. Bearer token in Authorization header (highest priority - user passthrough)
  2. MSL-Account header (explicit organization account selection)
  3. Project default account (configured account)

3. Comprehensive Header Passthrough

  • ALL client headers forwarded to Anthropic (except blacklist)
  • Preserves SDK telemetry, beta flags, and client identification
  • Blacklist includes only infrastructure headers (host, connection, proxy headers)
  • Prevents duplicate Authorization headers

4. Mandatory Project Identification

  • MSL-Project-Id header now mandatory for all requests
  • Removed fallback to 'default' project ID
  • Clear error messages when header missing

5. Enhanced Logging

  • Authorization headers logged in plain text for debugging
  • Separate logs for incoming vs outbound headers
  • Helps troubleshoot authentication issues

Technical Implementation

Database Migration

  • 015-user-passthrough-support.ts: Ensures default_account_id nullable with documentation

Core Changes

  • AuthenticationService: Reordered priority to check Bearer token first
  • ProxyRequest.createHeaders(): Blacklist-based header filtering
  • RequestContext: Captures all client headers, enforces mandatory MSL-Project-Id
  • ClaudeApiClient: Enhanced logging for debugging

Dashboard

  • Project creation dropdown includes "👤 User Account" option
  • Project list shows "User Account" badge when default_account_id = null
  • Can switch between organization accounts and user passthrough mode

Testing

  • New test: Bearer token takes priority over default account
  • Updated test: MSL-Project-Id leaves undefined when missing (no fallback)
  • All 38 proxy tests passing

Documentation

  • ADR-030: Complete specification of user passthrough mode
  • API Reference: Updated authentication priority and added user passthrough section
  • Header Blacklist: Fully documented which headers are filtered and why

Backward Compatibility

✅ Existing projects continue working unchanged
✅ Default behavior still assigns random organization account
✅ No breaking changes to API or database schema

Usage Example

# Using user's personal Anthropic credentials
curl https://api.yourdomain.com/v1/messages \
  -H "MSL-Project-Id: my-project" \
  -H "Authorization: Bearer sk-ant-api03-YOUR_ANTHROPIC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-3-5-sonnet-20241022",
    "messages": [{"role": "user", "content": "Hello!"}],
    "max_tokens": 1024
  }'

Related ADRs

  • ADR-004: Proxy Authentication
  • ADR-024: Header-Based Project and Account Routing
  • ADR-027: Mandatory User Authentication
  • ADR-030: User Credential Passthrough Mode (new)

Test Plan

  • Create project with "User Account" selected
  • Make request with Bearer token - succeeds
  • Make request without Bearer token - fails with clear error
  • Verify headers forwarded correctly (all client headers except blacklist)
  • Verify no duplicate Authorization headers
  • Verify Bearer token takes priority over default account
  • Verify MSL-Project-Id mandatory enforcement
  • Verify logging shows full authorization headers
  • All unit tests pass (38/38)
  • TypeScript compilation clean

🤖 Generated with Claude Code

crystalin and others added 15 commits November 4, 2025 17:21
Add support for projects to operate without a default organization account,
allowing users to provide their own Anthropic credentials via Authorization header.

**Changes:**
- Database: Migration 015 ensures default_account_id is nullable
- Types: Updated CreateProjectRequest and UpdateProjectRequest to accept null default_account_id
- Queries: Updated createProject/updateProject to handle null account explicitly
- Auth: AuthenticationService now has 3-tier priority:
  1. MSL-Account header (explicit account)
  2. Project default account
  3. User passthrough (Bearer token forwarding) [NEW]
- Dashboard: Added "User Account" option in project creation and management UI
- Tests: Added comprehensive tests for user passthrough authentication

**Documentation:**
- ADR-030: Documents decision, rationale, and implementation details
- Clear error messages when user credentials required but not provided

**Backward Compatibility:**
- Existing projects continue working unchanged
- Default behavior (random account assignment) preserved
- New projects can explicitly choose "User Account" mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Make the MSL-Project-Id header mandatory to ensure proper project identification,
especially critical for user passthrough mode where we need to look up whether
the project has a default account or requires user-provided credentials.

**Changes:**
- RequestContext.fromHono() now throws error if MSL-Project-Id header is missing
- Removed fallback to config.auth.defaultTrainId and unused import
- Updated ADR-030 to document mandatory header requirement
- Clear error message guides users to provide the required header

**Rationale:**
- User passthrough mode requires project lookup to determine authentication method
- Explicit project identification improves security and prevents ambiguity
- Makes API contract clearer and more predictable

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove the fallback to 'default' project ID in project-id-extractor middleware
to ensure MSL-Project-Id header is truly mandatory.

**Changes:**
- Removed fallback logic from projectIdExtractorMiddleware
- Removed unused imports (logger, config)
- If MSL-Project-Id header is missing, projectId remains unset
- RequestContext.fromHono() will catch and error with clear message

**Before:**
- Middleware set projectId to 'default' if header missing
- RequestContext never saw missing header
- Fallback behavior was implicit

**After:**
- Middleware only sets projectId if header present
- RequestContext throws explicit error if projectId missing
- Clear error message guides users

**Testing:**
- All authentication tests passing
- Typecheck passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add comprehensive logging of outbound requests to Anthropic API to help
debug authentication issues, especially for user passthrough mode.

**Changes:**
- Log full request details before sending to Anthropic
- Include URL, method, and all headers (except body for privacy)
- Mask Authorization token (show first 20 chars only)
- Helps diagnose authentication_error issues

**Log format:**
```
INFO: Outbound request to Anthropic API
  url: https://api.anthropic.com/v1/messages
  method: POST
  headers:
    Authorization: Bearer sk-ant-...
    anthropic-version: 2023-06-01
    anthropic-beta: oauth-2025-04-20
    Content-Type: application/json
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add maskToken helper to show last 4 chars of tokens
- Log incoming Authorization header from user
- Log all outbound headers sent to Anthropic
- Enable comparison of incoming vs outgoing headers for debugging

This helps diagnose authentication issues by showing what credentials
the proxy receives vs what it forwards to Anthropic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
In user passthrough mode, the proxy now forwards ALL headers sent by
the client (except infrastructure/proxy headers) to ensure proper
authentication and API compatibility.

Changes:
- Extract all client headers in RequestContext (not just selected ones)
- Implement header blacklist to filter proxy-specific headers
- Pass client headers through to Anthropic API via createHeaders()
- Update ClaudeApiClient.forward() signature to accept client headers
- Update test to reflect removal of 'default' project ID fallback

Blacklisted headers (not forwarded):
- host, connection, content-length, accept-encoding (infrastructure)
- baggage, sentry-trace, sec-fetch-mode (tracing/security)
- x-forwarded-for, x-real-ip (proxy headers)
- msl-project-id, msl-account, x-api-key (internal headers)

This ensures client headers like anthropic-beta, user-agent, x-stainless-*
are properly forwarded to Anthropic API for correct request handling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removed content-length and accept-encoding from the header blacklist
to allow these client headers to be forwarded to Anthropic API.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Restored content-length and accept-encoding to the header blacklist
to let fetch handle these headers automatically.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removed token masking from authorization headers in logs to enable
full debugging of authentication issues. Authorization headers are
now logged in plain text for both incoming and outbound requests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removed 'authorization' from the sensitive keys list and removed
Bearer token masking to enable full debugging of authentication issues.
Authorization headers are now logged in plain text.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Changed authentication priority to ensure user-provided Bearer tokens
always take precedence over project default accounts. This allows users
to use their own credentials even when a project has a default account.

New priority order:
1. MSL-Account header (explicit account selection)
2. Bearer token in Authorization header (user passthrough)
3. Project default account

Previously, the project default account would be used even when a user
provided their own Bearer token, which prevented user passthrough mode
from working correctly.

Added test to verify user token takes priority over default account.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Changed authentication priority to make user-provided Bearer tokens
the absolute highest priority, even above MSL-Account header.

New priority order:
1. Bearer token in Authorization header (user passthrough)
2. MSL-Account header (explicit account selection)
3. Project default account

This ensures user credentials are always respected first.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Added 'authorization' to the header blacklist to prevent duplicate
Authorization headers when forwarding requests to Anthropic API.

The client's authorization header (lowercase) was being included in
client headers, and then the auth service was adding Authorization
(capitalized) from authHeaders, resulting in both headers being sent.

Now the authorization header is filtered from client headers and only
the one from authHeaders is used, preventing duplicates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Updated documentation to reflect final implementation:

ADR-030 Updates:
- Changed authentication priority: Bearer token now highest priority
- Added comprehensive header passthrough section
- Documented blacklisted headers and rationale
- Added logging note about plain text authorization headers

API Reference Updates:
- Made MSL-Project-Id mandatory (no longer optional)
- Updated authentication priority order
- Added User Passthrough Mode section with examples
- Clarified billing and credential forwarding behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Moved baggage, sentry-trace, and sec-fetch-mode from blacklisted
to forwarded headers section. These tracing/security headers are now
forwarded to Anthropic API.

Updated header blacklist now only includes:
- Infrastructure headers (host, connection, content-length, accept-encoding)
- Proxy headers (x-forwarded-for, x-real-ip)
- Internal headers (msl-project-id, msl-account, x-api-key, authorization)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
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.

2 participants