- Rename tli/ directory to fxt/, update package + binary name to "fxt" - Replace all `use tli::` → `use fxt::` across 52 Rust files - Update build.rs proto paths (tli/proto → fxt/proto) in 6 services - Update Dockerfiles, CI workflows, deploy.sh for new paths - Delete ~170 legacy shell scripts (kept 15 essential ones) - Delete RunPod Python client (runpod/), tests (tests/runpod/) - Delete foxhunt-deploy crate (RunPod-only deployment tool) - Delete terraform/runpod/ (moved to Scaleway) - Delete ML Python hyperopt scripts (replaced by Rust Argmin PSO) - Delete .gitlab-ci.yml (using GitHub + Gitea) - Remove foxhunt-deploy from workspace members 504 files changed, -74,355 lines of legacy code removed. Workspace compiles clean (0 errors, 0 warnings). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 KiB
TLI Authentication via API Gateway (Wave 71)
Overview
TLI now connects exclusively through the API Gateway with JWT-based authentication. All connections use a single gRPC channel to the gateway, which handles authentication, authorization, and routing to backend services.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ TLI Client │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ AuthTokenManager │ │
│ │ • Access Token (in-memory) │ │
│ │ • Refresh Token (OS Keyring) │ │
│ │ • Automatic refresh on expiration │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ AuthInterceptor │ │
│ │ • Adds "Authorization: Bearer <token>" to requests │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Single gRPC Channel │ │
│ │ https://localhost:50050 │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ TLS/gRPC
▼
┌─────────────────────────────────────────────────────────────┐
│ API Gateway │
│ Port 50050 (HTTPS) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 6-Layer Authentication │ │
│ │ 1. JWT Validation │ │
│ │ 2. Token Revocation Check (Redis) │ │
│ │ 3. Authorization (RBAC) │ │
│ │ 4. Rate Limiting │ │
│ │ 5. Audit Logging │ │
│ │ 6. Request Routing │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Trading │ │ Backtesting │ │ ML Training │
│ Service │ │ Service │ │ Service │
│ Port 50051 │ │ Port 50053 │ │ Port 50054 │
└─────────────┘ └──────────────┘ └──────────────┘
Token Storage Strategy
Access Tokens (Short-lived, ~15 minutes)
- Storage: In-memory only
- Lifetime: 15 minutes (configurable)
- Purpose: Authenticate individual requests
- Security: Lost on process termination (by design)
Refresh Tokens (Long-lived, ~7 days)
- Storage: OS Keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service)
- Lifetime: 7 days (configurable)
- Purpose: Obtain new access tokens without re-login
- Security: Encrypted at rest by OS
Authentication Flow
1. Initial Login (Interactive)
use tli::auth::{LoginClient, AuthTokenManager, KeyringTokenStorage};
use tonic::transport::Channel;
#[tokio::main]
async fn main() -> Result<()> {
// Connect to API Gateway
let gateway_channel = Channel::from_static("https://localhost:50050")
.connect()
.await?;
// Create auth manager with OS keyring storage
let storage = KeyringTokenStorage::new(
"foxhunt-tli".to_string(),
"your_username".to_string()
);
let auth_manager = AuthTokenManager::new(storage);
// Create login client
let login_client = LoginClient::new(gateway_channel.clone());
// Perform interactive login (prompts for username/password)
login_client.interactive_login(&auth_manager).await?;
// Now ready to make authenticated requests
Ok(())
}
Login Prompt:
=== Foxhunt TLI Authentication ===
Username: trader_john
Password: ********
✅ Authentication successful!
2. MFA Flow (If Enabled)
=== Foxhunt TLI Authentication ===
Username: trader_john
Password: ********
🔐 Multi-Factor Authentication Required
Enter TOTP code from your authenticator app: 123456
✅ MFA verification successful!
3. Silent Login (Automatic)
On subsequent TLI launches, the client attempts silent authentication using the stored refresh token:
// Attempt silent login first
if !login_client.silent_login(&auth_manager).await? {
// Fallback to interactive login if silent fails
login_client.interactive_login(&auth_manager).await?;
}
4. Automatic Token Refresh
The AuthInterceptor automatically includes the access token in every request. If a request fails with UNAUTHENTICATED status, the client automatically refreshes:
// This happens automatically in the background
match trading_client.place_order(request).await {
Err(status) if status.code() == Code::Unauthenticated => {
// Automatic refresh triggered
login_client.refresh_tokens(&auth_manager).await?;
// Retry the request
trading_client.place_order(request).await?
}
Ok(response) => Ok(response),
Err(e) => Err(e),
}
Example: Complete TLI Setup with Authentication
use tli::auth::{AuthTokenManager, KeyringTokenStorage, AuthInterceptor, LoginClient};
use tli::client::{TradingClient, TradingClientConfig};
use tonic::transport::Channel;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 1. Connect to API Gateway
let gateway_channel = Channel::from_static("https://localhost:50050")
.tls_config(tonic::transport::ClientTlsConfig::new())?
.connect()
.await?;
// 2. Create authentication manager
let storage = KeyringTokenStorage::new(
"foxhunt-tli".to_string(),
whoami::username(), // Use system username
);
let auth_manager = AuthTokenManager::new(storage);
// 3. Authenticate (silent login first, then interactive if needed)
let login_client = LoginClient::new(gateway_channel.clone());
if !login_client.silent_login(&auth_manager).await? {
login_client.interactive_login(&auth_manager).await?;
}
// 4. Create auth interceptor
let auth_interceptor = AuthInterceptor::new(auth_manager.clone());
// 5. Create authenticated gRPC channel
let authenticated_channel = tower::ServiceBuilder::new()
.layer(tonic::service::interceptor(auth_interceptor))
.service(gateway_channel);
// 6. Create service clients using authenticated channel
let mut trading_client = TradingClient::new(TradingClientConfig {
endpoint: "https://localhost:50050".to_string(),
timeout_ms: 30_000,
});
trading_client.connect().await?;
// 7. Make authenticated requests
// All requests automatically include JWT Bearer token
// let response = trading_client.get_account_info(...).await?;
println!("✅ TLI connected and authenticated via API Gateway");
Ok(())
}
Security Features
1. OS Keyring Integration
- macOS: Keychain Access
- Windows: Credential Manager
- Linux: Secret Service (GNOME Keyring, KWallet)
Tokens are encrypted at rest by the operating system's secure storage mechanism.
2. Token Rotation
The API Gateway can rotate refresh tokens on each use, invalidating old tokens automatically.
3. Automatic Expiration Handling
Access tokens are checked for expiration before use. If expired within 60 seconds, automatic refresh is triggered.
4. Secure Password Input
Passwords are never echoed to the terminal using rpassword crate.
5. TLS Enforcement
All connections require HTTPS. HTTP connections are rejected with security errors.
Environment Variables
# API Gateway endpoint
API_GATEWAY_URL=https://localhost:50050
# JWT token storage path (optional override)
TLI_JWT_TOKEN_PATH=/home/user/.foxhunt/jwt_token
# Enable debug logging
RUST_LOG=tli=debug,tli::auth=trace
Development vs Production
Development (In-Memory Storage)
use tli::auth::InMemoryTokenStorage;
let storage = InMemoryTokenStorage::new();
let auth_manager = AuthTokenManager::new(storage);
Warning: Tokens are lost on process termination. Only use for testing.
Production (OS Keyring)
use tli::auth::KeyringTokenStorage;
let storage = KeyringTokenStorage::new(
"foxhunt-tli".to_string(),
username.to_string()
);
let auth_manager = AuthTokenManager::new(storage);
Logout
To logout and clear all tokens:
auth_manager.clear_tokens().await?;
println!("✅ Logged out - all tokens cleared");
This removes both the in-memory access token and the stored refresh token from the OS keyring.
Troubleshooting
"No refresh token available"
- Run interactive login:
login_client.interactive_login(&auth_manager).await?
"Failed to store refresh token in OS keyring"
- macOS: Grant TLI access to Keychain
- Linux: Ensure GNOME Keyring or KWallet daemon is running
- Windows: Check Credential Manager permissions
"UNAUTHENTICATED" errors
- Token may be expired or revoked
- Run
auth_manager.clear_tokens()and re-login
"Invalid token format"
- JWT may be corrupted
- Clear tokens and re-authenticate
Migration from Wave 69 (Direct Connections)
Before (Wave 69):
// Direct connections to each service
let trading_config = TradingClientConfig {
endpoint: "https://localhost:50051".to_string(), // Trading Service
timeout_ms: 30_000,
};
let backtesting_config = BacktestingClientConfig {
endpoint: "https://localhost:50053".to_string(), // Backtesting Service
timeout_ms: 60_000,
};
After (Wave 71):
// All services via API Gateway
let trading_config = TradingClientConfig {
endpoint: "https://localhost:50050".to_string(), // API Gateway
timeout_ms: 30_000,
};
let backtesting_config = BacktestingClientConfig {
endpoint: "https://localhost:50050".to_string(), // API Gateway
timeout_ms: 60_000,
};
// Authentication required
let auth_manager = AuthTokenManager::new(storage);
login_client.interactive_login(&auth_manager).await?;
API Gateway Routing
The API Gateway routes based on gRPC service names in the request:
| Service Name | Backend Service | Backend Port |
|---|---|---|
foxhunt.trading.* |
Trading Service | 50051 |
foxhunt.backtesting.* |
Backtesting Service | 50053 |
foxhunt.ml.* |
ML Training Service | 50054 |
All routing is transparent to TLI - clients use the same service names as before.
Performance
- Authentication overhead: <10μs per request (cached JWT validation)
- Token refresh: Automatic background operation
- Connection reuse: Single HTTP/2 connection multiplexed for all services
Wave 71 - TLI API Gateway Integration Complete