Skip to content

Commit 10bb478

Browse files
committed
feat: add automatic JWT token refresh implementation
1 parent efae037 commit 10bb478

File tree

13 files changed

+1856
-42
lines changed

13 files changed

+1856
-42
lines changed

AGENTS.md

Lines changed: 165 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,17 +242,42 @@ type Credential interface {
242242

243243
**Implementation Details:**
244244
- JWT tokens are parsed ONLY to extract provider information (for display purposes)
245-
- No client-side expiration checking - the API server is the source of truth for token validity
246-
- When API returns 401 Unauthorized, users receive helpful error message with guidance
245+
- **Automatic token refresh**: When API returns 401 Unauthorized, the server automatically refreshes the token
246+
- **File watching**: The server watches the credential file and reloads tokens when Terramate CLI updates them
247+
- **Thread-safe**: All credential operations use mutex protection for concurrent access
248+
- **Atomic file updates**: Credential file updates are atomic to prevent corruption
247249
- Uses `Authorization: Bearer <token>` header
248-
- No automatic refresh in MCP server (users must re-run `terramate cloud login`)
250+
- **Zero maintenance**: No manual token refresh or server restarts needed
251+
252+
**Automatic Token Refresh:**
253+
The MCP server implements a hybrid approach for seamless token management:
254+
1. **Reactive Refresh**: When API returns 401, server refreshes token and retries request
255+
2. **File Watching**: Server watches `~/.terramate.d/credentials.tmrc.json` for external updates
256+
3. **Shared Credentials**: Both MCP server and Terramate CLI safely share the same credential file
257+
4. **Atomic Updates**: File updates use atomic operations to prevent race conditions
258+
259+
**How it Works:**
260+
```
261+
┌─────────────────┐ ┌──────────────────┐
262+
│ Terramate CLI │◄───────►│ Credential File │
263+
│ (Token Manager)│ writes │ (Shared State) │
264+
└─────────────────┘ └──────────────────┘
265+
266+
│ watches
267+
│ & reads
268+
269+
┌──────────────────┐
270+
│ MCP Server │
271+
│ (Auto-Refresh) │
272+
└──────────────────┘
273+
```
249274

250275
**Security Note:**
251276
The client does NOT validate JWT expiration locally. This is intentional and follows security best practices:
252277
- Client-side parsing uses `ParseUnverified()` which doesn't verify signatures
253278
- Making security decisions based on unverified data would be unsafe
254279
- The API server is the authoritative source for token validation
255-
- 401 errors from API provide clear guidance to users to refresh credentials
280+
- 401 errors trigger automatic token refresh - transparent to users
256281

257282
### API Key Authentication (issuing an organization API key requires admin privileges)
258283

@@ -344,4 +369,139 @@ func (s *Service) SomeMethod(ctx context.Context, ...) error {
344369
- The SDK will refuse to load credential files with insecure permissions
345370
- Never commit credential files to git
346371
- `.terramate.d/` should be in `.gitignore`
347-
- MCP server reads credentials on startup only (not monitored for changes)
372+
- MCP server watches the credential file for changes and automatically reloads tokens
373+
374+
## Security Best Practices
375+
376+
### 🔒 Preventing Token Leakage
377+
378+
**CRITICAL: Never expose tokens, API keys, or credentials in:**
379+
- Error messages
380+
- Log messages
381+
- Debug output
382+
- HTTP response bodies in error messages
383+
- Stack traces
384+
- Test output (unless sanitized)
385+
386+
**When handling errors:**
387+
```go
388+
// ❌ BAD: Leaks token in error message
389+
return fmt.Errorf("refresh failed: %s", string(responseBody))
390+
391+
// ✅ GOOD: Parse JSON safely, extract only safe fields
392+
var errResp struct {
393+
Error string `json:"error"`
394+
}
395+
if err := json.Unmarshal(body, &errResp); err == nil {
396+
return fmt.Errorf("refresh failed: %s", errResp.Error)
397+
}
398+
return fmt.Errorf("refresh failed (status %d)", statusCode)
399+
```
400+
401+
**When logging:**
402+
```go
403+
// ❌ BAD: Logs token value
404+
log.Printf("Token: %s", token)
405+
406+
// ✅ GOOD: Generic log message
407+
log.Printf("JWT token refreshed successfully")
408+
409+
// ✅ GOOD: Log error without token
410+
log.Printf("Warning: failed to reload credential: %v", err)
411+
```
412+
413+
**When handling HTTP responses:**
414+
```go
415+
// ❌ BAD: Includes raw body in error (may contain tokens)
416+
apiErr := &APIError{Message: string(body)}
417+
418+
// ✅ GOOD: Parse JSON safely, extract only error fields
419+
apiErr := &APIError{Message: "API request failed"}
420+
if isJSONContentType(resp.Header.Get("Content-Type")) {
421+
var errResp ErrorResponse
422+
if err := json.Unmarshal(body, &errResp); err == nil {
423+
apiErr.Message = errResp.Error // Only safe parsed field
424+
}
425+
}
426+
```
427+
428+
### Security Checklist for New Code
429+
430+
When adding or modifying code that handles credentials:
431+
432+
- [ ] **Error Messages**: Never include tokens, API keys, or raw HTTP response bodies
433+
- [ ] **Logging**: Use generic messages, never log credential values
434+
- [ ] **JSON Parsing**: Parse error responses safely, extract only known safe fields
435+
- [ ] **HTTP Bodies**: Never convert response bodies to strings for error messages without parsing
436+
- [ ] **Test Output**: In tests, only log token prefixes (e.g., `token[:20]+"..."`) if needed
437+
- [ ] **File Permissions**: Always validate credential file permissions (`0600`)
438+
- [ ] **Thread Safety**: Use mutexes for concurrent credential access
439+
- [ ] **Input Validation**: Validate all inputs before processing
440+
- [ ] **HTTPS Only**: Never use HTTP for credential transmission
441+
- [ ] **Context Timeouts**: Use context timeouts for all network operations
442+
443+
### Common Security Anti-Patterns to Avoid
444+
445+
**1. Token Leakage in Errors:**
446+
```go
447+
// ❌ BAD
448+
return fmt.Errorf("failed: %s", string(httpResponseBody))
449+
450+
// ✅ GOOD
451+
return fmt.Errorf("failed: %s", parseSafeError(httpResponseBody))
452+
```
453+
454+
**2. Logging Credentials:**
455+
```go
456+
// ❌ BAD
457+
log.Printf("Using token: %s", token)
458+
459+
// ✅ GOOD
460+
log.Printf("Using JWT authentication")
461+
```
462+
463+
**3. Including Raw Bodies:**
464+
```go
465+
// ❌ BAD
466+
err := fmt.Errorf("API error: %s", string(body))
467+
468+
// ✅ GOOD
469+
err := parseAPIError(resp, body) // Safely parses JSON
470+
```
471+
472+
**4. Debug Output:**
473+
```go
474+
// ❌ BAD
475+
fmt.Printf("Token: %v\n", credential)
476+
477+
// ✅ GOOD
478+
fmt.Printf("Credential type: %s\n", credential.Name())
479+
```
480+
481+
### Security Review Process
482+
483+
Before committing code that handles credentials:
484+
485+
1. **Search for token leakage:**
486+
```bash
487+
grep -r "fmt.*token\|log.*token\|string(body)" --include="*.go"
488+
```
489+
490+
2. **Verify error handling:**
491+
- Check all `fmt.Errorf()` calls with `%s` or `%v` formatting
492+
- Ensure HTTP response bodies are parsed, not converted to strings
493+
- Verify error messages don't include credential values
494+
495+
3. **Check logging:**
496+
- Search for `log.Printf` or `log.Println` with credential variables
497+
- Ensure all log messages are generic
498+
499+
4. **Test security:**
500+
- Run tests and verify no tokens appear in output
501+
- Check error messages don't expose sensitive data
502+
- Verify file permissions are enforced
503+
504+
5. **Review HTTP handling:**
505+
- Ensure all API calls use HTTPS
506+
- Verify response bodies are parsed safely
507+
- Check error handling doesn't leak response bodies

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Add automatic JWT token refresh when API returns 401 Unauthorized
12+
- Add file watching to automatically reload credentials when Terramate CLI updates them
13+
- Add thread-safe credential management with mutex protection
14+
- Add `StartWatching()` and `StopWatching()` methods for credential file monitoring
15+
- Add comprehensive test suite for token refresh and file watching functionality
16+
17+
### Changed
18+
- JWT credentials now automatically refresh expired tokens without user intervention
19+
- MCP server and Terramate CLI can now safely share and update the same credential file
20+
- Credential file updates are atomic to prevent corruption during concurrent access
21+
22+
### Fixed
23+
- Fix US region endpoint from `api.us.terramate.io` to `us.api.terramate.io`
24+
1025
## [0.0.2] - 2025-11-13
1126

1227
### Added

0 commit comments

Comments
 (0)