Files
foxhunt/fxt/docs/AUTHENTICATION.md
jgrusewski 2da5bafc0e refactor: rename tli→fxt, delete legacy scripts/RunPod/deploy artifacts
- 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>
2026-02-24 10:32:21 +01:00

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