Skip to content

Commit 3fe2c1b

Browse files
authored
feat: implement WebSocket authentication system with UUID tokens (#56)
1 parent 3f09b51 commit 3fe2c1b

File tree

16 files changed

+1177
-38
lines changed

16 files changed

+1177
-38
lines changed

CLAUDE.md

Lines changed: 147 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,16 @@ claudecode.nvim - A Neovim plugin that implements the same WebSocket-based MCP p
2222

2323
### Build Commands
2424

25+
- `make` - **RECOMMENDED**: Run formatting, linting, and testing (complete validation)
2526
- `make all` - Run check and format (default target)
27+
- `make test` - Run all tests using busted with coverage
28+
- `make check` - Check Lua syntax and run luacheck
29+
- `make format` - Format code with stylua (or nix fmt if available)
2630
- `make clean` - Remove generated test files
2731
- `make help` - Show available commands
2832

33+
**Best Practice**: Always use `make` at the end of editing sessions for complete validation.
34+
2935
### Development with Nix
3036

3137
- `nix develop` - Enter development shell with all dependencies
@@ -45,11 +51,20 @@ claudecode.nvim - A Neovim plugin that implements the same WebSocket-based MCP p
4551
### WebSocket Server Implementation
4652

4753
- **TCP Server**: `server/tcp.lua` handles port binding and connections
48-
- **Handshake**: `server/handshake.lua` processes HTTP upgrade requests
54+
- **Handshake**: `server/handshake.lua` processes HTTP upgrade requests with authentication
4955
- **Frame Processing**: `server/frame.lua` implements RFC 6455 WebSocket frames
5056
- **Client Management**: `server/client.lua` manages individual connections
5157
- **Utils**: `server/utils.lua` provides base64, SHA-1, XOR operations in pure Lua
5258

59+
#### Authentication System
60+
61+
The WebSocket server implements secure authentication using:
62+
63+
- **UUID v4 Tokens**: Generated per session with enhanced entropy
64+
- **Header-based Auth**: Uses `x-claude-code-ide-authorization` header
65+
- **Lock File Discovery**: Tokens stored in `~/.claude/ide/[port].lock` for Claude CLI
66+
- **MCP Compliance**: Follows official Claude Code IDE authentication protocol
67+
5368
### MCP Tools Architecture
5469

5570
Tools are registered with JSON schemas and handlers. MCP-exposed tools include:
@@ -76,14 +91,125 @@ Tests are organized in three layers:
7691

7792
Test files follow the pattern `*_spec.lua` or `*_test.lua` and use the busted framework.
7893

94+
### Test Organization Principles
95+
96+
- **Isolation**: Each test should be independent and not rely on external state
97+
- **Mocking**: Use comprehensive mocking for vim APIs and external dependencies
98+
- **Coverage**: Aim for both positive and negative test cases, edge cases included
99+
- **Performance**: Tests should run quickly to encourage frequent execution
100+
- **Clarity**: Test names should clearly describe what behavior is being verified
101+
102+
## Authentication Testing
103+
104+
The plugin implements authentication using UUID v4 tokens that are generated for each server session and stored in lock files. This ensures secure connections between Claude CLI and the Neovim WebSocket server.
105+
106+
### Testing Authentication Features
107+
108+
**Lock File Authentication Tests** (`tests/lockfile_test.lua`):
109+
110+
- Auth token generation and uniqueness validation
111+
- Lock file creation with authentication tokens
112+
- Reading auth tokens from existing lock files
113+
- Error handling for missing or invalid tokens
114+
115+
**WebSocket Handshake Authentication Tests** (`tests/unit/server/handshake_spec.lua`):
116+
117+
- Valid authentication token acceptance
118+
- Invalid/missing token rejection
119+
- Edge cases (empty tokens, malformed headers, length limits)
120+
- Case-insensitive header handling
121+
122+
**Server Integration Tests** (`tests/unit/server_spec.lua`):
123+
124+
- Server startup with authentication tokens
125+
- Auth token state management during server lifecycle
126+
- Token validation throughout server operations
127+
128+
**End-to-End Authentication Tests** (`tests/integration/mcp_tools_spec.lua`):
129+
130+
- Complete authentication flow from server start to tool execution
131+
- Authentication state persistence across operations
132+
- Concurrent operations with authentication enabled
133+
134+
### Manual Authentication Testing
135+
136+
**Test Script Authentication Support**:
137+
138+
```bash
139+
# Test scripts automatically detect and use authentication tokens
140+
cd scripts/
141+
./claude_interactive.sh # Automatically reads auth token from lock file
142+
```
143+
144+
**Authentication Flow Testing**:
145+
146+
1. Start the plugin: `:ClaudeCodeStart`
147+
2. Check lock file contains `authToken`: `cat ~/.claude/ide/*.lock | jq .authToken`
148+
3. Test WebSocket connection with auth: Use test scripts in `scripts/` directory
149+
4. Verify authentication in logs: Set `log_level = "debug"` in config
150+
151+
**Testing Authentication Failures**:
152+
153+
```bash
154+
# Test invalid auth token (should fail)
155+
websocat ws://localhost:PORT --header "x-claude-code-ide-authorization: invalid-token"
156+
157+
# Test missing auth header (should fail)
158+
websocat ws://localhost:PORT
159+
160+
# Test valid auth token (should succeed)
161+
websocat ws://localhost:PORT --header "x-claude-code-ide-authorization: $(cat ~/.claude/ide/*.lock | jq -r .authToken)"
162+
```
163+
164+
### Authentication Logging
165+
166+
Enable detailed authentication logging by setting:
167+
168+
```lua
169+
require("claudecode").setup({
170+
log_level = "debug" -- Shows auth token generation, validation, and failures
171+
})
172+
```
173+
174+
Log levels for authentication events:
175+
176+
- **DEBUG**: Server startup authentication state, client connections, handshake processing, auth token details
177+
- **WARN**: Authentication failures during handshake
178+
- **ERROR**: Auth token generation failures, handshake response errors
179+
180+
### Logging Best Practices
181+
182+
- **Connection Events**: Use DEBUG level for routine connection establishment/teardown
183+
- **Authentication Flow**: Use DEBUG for successful auth, WARN for failures
184+
- **User-Facing Events**: Use INFO sparingly for events users need to know about
185+
- **System Errors**: Use ERROR for failures that require user attention
186+
79187
## Development Notes
80188

189+
### Technical Requirements
190+
81191
- Plugin requires Neovim >= 0.8.0
82192
- Uses only Neovim built-ins for WebSocket implementation (vim.loop, vim.json, vim.schedule)
83-
- Lock files are created at `~/.claude/ide/[port].lock` for Claude CLI discovery
84-
- WebSocket server only accepts local connections for security
193+
- Zero external dependencies for core functionality
194+
195+
### Security Considerations
196+
197+
- WebSocket server only accepts local connections (127.0.0.1) for security
198+
- Authentication tokens are UUID v4 with enhanced entropy
199+
- Lock files created at `~/.claude/ide/[port].lock` for Claude CLI discovery
200+
- All authentication events are logged for security auditing
201+
202+
### Performance Optimizations
203+
85204
- Selection tracking is debounced to reduce overhead
205+
- WebSocket frame processing optimized for JSON-RPC payload sizes
206+
- Connection pooling and cleanup to prevent resource leaks
207+
208+
### Integration Support
209+
86210
- Terminal integration supports both snacks.nvim and native Neovim terminal
211+
- Compatible with popular file explorers (nvim-tree, oil.nvim)
212+
- Visual selection tracking across different selection modes
87213

88214
## Release Process
89215

@@ -134,6 +260,23 @@ make
134260
rg "0\.1\.0" . # Should only show CHANGELOG.md historical entries
135261
```
136262

137-
## CRITICAL: Pre-commit Requirements
263+
## Development Workflow
264+
265+
### Pre-commit Requirements
138266

139267
**ALWAYS run `make` before committing any changes.** This runs code quality checks and formatting that must pass for CI to succeed. Never skip this step - many PRs fail CI because contributors don't run the build commands before committing.
268+
269+
### Recommended Development Flow
270+
271+
1. **Start Development**: Use existing tests and documentation to understand the system
272+
2. **Make Changes**: Follow existing patterns and conventions in the codebase
273+
3. **Validate Work**: Run `make` to ensure formatting, linting, and tests pass
274+
4. **Document Changes**: Update relevant documentation (this file, PROTOCOL.md, etc.)
275+
5. **Commit**: Only commit after successful `make` execution
276+
277+
### Code Quality Standards
278+
279+
- **Test Coverage**: Maintain comprehensive test coverage (currently 314+ tests)
280+
- **Zero Warnings**: All code must pass luacheck with 0 warnings/errors
281+
- **Consistent Formatting**: Use `nix fmt` or `stylua` for consistent code style
282+
- **Documentation**: Update CLAUDE.md for architectural changes, PROTOCOL.md for protocol changes

PROTOCOL.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ The IDE writes a discovery file to `~/.claude/ide/[port].lock`:
2323
"pid": 12345, // IDE process ID
2424
"workspaceFolders": ["/path/to/project"], // Open folders
2525
"ideName": "VS Code", // or "Neovim", "IntelliJ", etc.
26-
"transport": "ws" // WebSocket transport
26+
"transport": "ws", // WebSocket transport
27+
"authToken": "550e8400-e29b-41d4-a716-446655440000" // Random UUID for authentication
2728
}
2829
```
2930

@@ -38,6 +39,16 @@ When launching Claude, the IDE sets:
3839

3940
Claude reads the lock files, finds the matching port from the environment, and connects to the WebSocket server.
4041

42+
## Authentication
43+
44+
When Claude connects to the IDE's WebSocket server, it must authenticate using the token from the lock file. The authentication happens via a custom WebSocket header:
45+
46+
```
47+
x-claude-code-ide-authorization: 550e8400-e29b-41d4-a716-446655440000
48+
```
49+
50+
The IDE validates this header against the `authToken` value from the lock file. If the token doesn't match, the connection is rejected.
51+
4152
## The Protocol
4253

4354
Communication uses WebSocket with JSON-RPC 2.0 messages:
@@ -503,11 +514,13 @@ local server = create_websocket_server("127.0.0.1", random_port)
503514

504515
```lua
505516
-- ~/.claude/ide/[port].lock
517+
local auth_token = generate_uuid() -- Generate random UUID
506518
local lock_data = {
507519
pid = vim.fn.getpid(),
508520
workspaceFolders = { vim.fn.getcwd() },
509521
ideName = "YourEditor",
510-
transport = "ws"
522+
transport = "ws",
523+
authToken = auth_token
511524
}
512525
write_json(lock_path, lock_data)
513526
```
@@ -523,6 +536,12 @@ claude # Claude will now connect!
523536
### 4. Handle Messages
524537

525538
```lua
539+
-- Validate authentication on WebSocket handshake
540+
function validate_auth(headers)
541+
local auth_header = headers["x-claude-code-ide-authorization"]
542+
return auth_header == auth_token
543+
end
544+
526545
-- Send selection updates
527546
send_message({
528547
jsonrpc = "2.0",

lua/claudecode/init.lua

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ local default_config = {
6767
--- @field config ClaudeCode.Config The current plugin configuration.
6868
--- @field server table|nil The WebSocket server instance.
6969
--- @field port number|nil The port the server is running on.
70+
--- @field auth_token string|nil The authentication token for the current session.
7071
--- @field initialized boolean Whether the plugin has been initialized.
7172
--- @field queued_mentions table[] Array of queued @ mentions waiting for connection.
7273
--- @field connection_timer table|nil Timer for connection timeout.
@@ -76,6 +77,7 @@ M.state = {
7677
config = vim.deepcopy(default_config),
7778
server = nil,
7879
port = nil,
80+
auth_token = nil,
7981
initialized = false,
8082
queued_mentions = {},
8183
connection_timer = nil,
@@ -357,26 +359,70 @@ function M.start(show_startup_notification)
357359
end
358360

359361
local server = require("claudecode.server.init")
360-
local success, result = server.start(M.state.config)
362+
local lockfile = require("claudecode.lockfile")
363+
364+
-- Generate auth token first so we can pass it to the server
365+
local auth_token
366+
local auth_success, auth_result = pcall(function()
367+
return lockfile.generate_auth_token()
368+
end)
369+
370+
if not auth_success then
371+
local error_msg = "Failed to generate authentication token: " .. (auth_result or "unknown error")
372+
logger.error("init", error_msg)
373+
return false, error_msg
374+
end
375+
376+
auth_token = auth_result
377+
378+
-- Validate the generated auth token
379+
if not auth_token or type(auth_token) ~= "string" or #auth_token < 10 then
380+
local error_msg = "Invalid authentication token generated"
381+
logger.error("init", error_msg)
382+
return false, error_msg
383+
end
384+
385+
local success, result = server.start(M.state.config, auth_token)
361386

362387
if not success then
363-
logger.error("init", "Failed to start Claude Code integration: " .. result)
364-
return false, result
388+
local error_msg = "Failed to start Claude Code server: " .. (result or "unknown error")
389+
if result and result:find("auth") then
390+
error_msg = error_msg .. " (authentication related)"
391+
end
392+
logger.error("init", error_msg)
393+
return false, error_msg
365394
end
366395

367396
M.state.server = server
368397
M.state.port = tonumber(result)
398+
M.state.auth_token = auth_token
369399

370-
local lockfile = require("claudecode.lockfile")
371-
local lock_success, lock_result = lockfile.create(M.state.port)
400+
local lock_success, lock_result, returned_auth_token = lockfile.create(M.state.port, auth_token)
372401

373402
if not lock_success then
374403
server.stop()
375404
M.state.server = nil
376405
M.state.port = nil
406+
M.state.auth_token = nil
407+
408+
local error_msg = "Failed to create lock file: " .. (lock_result or "unknown error")
409+
if lock_result and lock_result:find("auth") then
410+
error_msg = error_msg .. " (authentication token issue)"
411+
end
412+
logger.error("init", error_msg)
413+
return false, error_msg
414+
end
377415

378-
logger.error("init", "Failed to create lock file: " .. lock_result)
379-
return false, lock_result
416+
-- Verify that the auth token in the lock file matches what we generated
417+
if returned_auth_token ~= auth_token then
418+
server.stop()
419+
M.state.server = nil
420+
M.state.port = nil
421+
M.state.auth_token = nil
422+
423+
local error_msg = "Authentication token mismatch between server and lock file"
424+
logger.error("init", error_msg)
425+
return false, error_msg
380426
end
381427

382428
if M.state.config.track_selection then
@@ -422,6 +468,7 @@ function M.stop()
422468

423469
M.state.server = nil
424470
M.state.port = nil
471+
M.state.auth_token = nil
425472

426473
-- Clear any queued @ mentions when server stops
427474
clear_mention_queue()

0 commit comments

Comments
 (0)