From 06cddd85d2f5a747e66a1401d92728750053863c Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 4 Mar 2026 01:21:07 +0100 Subject: [PATCH] fix(fxt): wire auth interceptor into all gRPC clients FoxhuntClient now holds an AuthInterceptor and all 11 typed service accessors use with_interceptor() instead of bare Channel::new(). This ensures every outgoing RPC automatically includes the JWT Bearer token when one is cached on disk. The bare channel() accessor is preserved for LoginClient which needs unauthenticated access for the initial login RPC. Co-Authored-By: Claude Opus 4.6 --- bin/fxt/src/grpc.rs | 106 +++++++++++++++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 27 deletions(-) diff --git a/bin/fxt/src/grpc.rs b/bin/fxt/src/grpc.rs index 0c455a3a9..b1652a529 100644 --- a/bin/fxt/src/grpc.rs +++ b/bin/fxt/src/grpc.rs @@ -3,24 +3,37 @@ //! All service RPCs route through a single [`FoxhuntClient`] which holds //! one lazily-connected [`Channel`] to the API Gateway. Individual typed //! clients (trading, ML, risk, ...) are constructed on demand from the -//! shared channel. +//! shared channel, each wrapped with [`AuthInterceptor`] so every outgoing +//! RPC carries the JWT Bearer token when available. use anyhow::Result; +use tonic::service::interceptor::InterceptedService; use tonic::transport::Channel; -/// Unified gRPC client -- all service accessors share a single channel. +use crate::auth::interceptor::AuthInterceptor; +use crate::auth::token_manager::{AuthTokenManager, FileTokenStorage}; + +/// Channel wrapped with the JWT auth interceptor. +type AuthChannel = InterceptedService>; + +/// Unified gRPC client -- all service accessors share a single channel +/// and automatically inject JWT auth headers via [`AuthInterceptor`]. pub struct FoxhuntClient { channel: Channel, + interceptor: AuthInterceptor, } impl FoxhuntClient { /// Create a client that will lazily connect to the API Gateway. /// + /// Sets up the auth interceptor backed by [`FileTokenStorage`] so all + /// outgoing RPCs include the Bearer token when one is cached on disk. + /// /// The TCP handshake is deferred until the first RPC call (`connect_lazy`). /// /// # Errors /// - /// Returns an error if the URL cannot be parsed as a valid URI. + /// Returns an error if the URL cannot be parsed or token storage init fails. pub async fn connect(api_url: &str) -> Result { let mut endpoint = Channel::from_shared(api_url.to_owned()).map_err(|e| anyhow::anyhow!("{e}"))?; @@ -33,10 +46,24 @@ impl FoxhuntClient { } let channel = endpoint.connect_lazy(); - Ok(Self { channel }) + + // Auth interceptor: reads cached JWT from ~/.config/foxhunt-tli/tokens/ + // and injects "Authorization: Bearer " on every RPC. + // If no token is cached, requests proceed without auth (allows login). + let storage = FileTokenStorage::new()?; + let auth_manager = AuthTokenManager::new(storage); + let interceptor = AuthInterceptor::new(auth_manager); + + Ok(Self { + channel, + interceptor, + }) } - /// Get the underlying channel for creating typed service clients. + /// Get the underlying bare channel (no auth interceptor). + /// + /// Used by [`LoginClient`] which needs unauthenticated access for the + /// initial login RPC before any token exists. pub fn channel(&self) -> Channel { self.channel.clone() } @@ -46,83 +73,107 @@ impl FoxhuntClient { /// Trading service client (order management, positions, executions). pub fn trading( &self, - ) -> crate::proto::trading::trading_service_client::TradingServiceClient { - crate::proto::trading::trading_service_client::TradingServiceClient::new( + ) -> crate::proto::trading::trading_service_client::TradingServiceClient { + crate::proto::trading::trading_service_client::TradingServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// ML inference service client (predictions, ensemble voting). - pub fn ml(&self) -> crate::proto::ml::ml_service_client::MlServiceClient { - crate::proto::ml::ml_service_client::MlServiceClient::new(self.channel.clone()) + pub fn ml(&self) -> crate::proto::ml::ml_service_client::MlServiceClient { + crate::proto::ml::ml_service_client::MlServiceClient::with_interceptor( + self.channel.clone(), + self.interceptor.clone(), + ) } /// ML training service client (start/stop training, checkpoints). pub fn ml_training( &self, - ) -> crate::proto::ml_training::ml_training_service_client::MlTrainingServiceClient + ) -> crate::proto::ml_training::ml_training_service_client::MlTrainingServiceClient { - crate::proto::ml_training::ml_training_service_client::MlTrainingServiceClient::new( + crate::proto::ml_training::ml_training_service_client::MlTrainingServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Broker gateway service client (account info, order routing). pub fn broker_gateway( &self, - ) -> crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient + ) -> crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient { - crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient::new( + crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Trading agent service client (autonomous agent control). pub fn trading_agent( &self, - ) -> crate::proto::trading_agent::trading_agent_service_client::TradingAgentServiceClient + ) -> crate::proto::trading_agent::trading_agent_service_client::TradingAgentServiceClient { - crate::proto::trading_agent::trading_agent_service_client::TradingAgentServiceClient::new( + crate::proto::trading_agent::trading_agent_service_client::TradingAgentServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Monitoring service client (metrics, alerts, system health). pub fn monitoring( &self, - ) -> crate::proto::monitoring::monitoring_service_client::MonitoringServiceClient { - crate::proto::monitoring::monitoring_service_client::MonitoringServiceClient::new( + ) -> crate::proto::monitoring::monitoring_service_client::MonitoringServiceClient + { + crate::proto::monitoring::monitoring_service_client::MonitoringServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Configuration service client (trading service config -- package `config`). pub fn config( &self, - ) -> crate::proto::config::config_service_client::ConfigServiceClient { - crate::proto::config::config_service_client::ConfigServiceClient::new( + ) -> crate::proto::config::config_service_client::ConfigServiceClient { + crate::proto::config::config_service_client::ConfigServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Data acquisition service client (market data feeds, downloads). pub fn data_acquisition( &self, - ) -> crate::proto::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient + ) -> crate::proto::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient { - crate::proto::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient::new( + crate::proto::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Risk service client (VaR, position limits, circuit breakers). - pub fn risk(&self) -> crate::proto::risk::risk_service_client::RiskServiceClient { - crate::proto::risk::risk_service_client::RiskServiceClient::new(self.channel.clone()) + pub fn risk( + &self, + ) -> crate::proto::risk::risk_service_client::RiskServiceClient { + crate::proto::risk::risk_service_client::RiskServiceClient::with_interceptor( + self.channel.clone(), + self.interceptor.clone(), + ) } /// gRPC health check client (standard `grpc.health.v1.Health`). - pub fn health(&self) -> crate::proto::health::health_client::HealthClient { - crate::proto::health::health_client::HealthClient::new(self.channel.clone()) + /// + /// Health checks don't require auth, but the interceptor handles the + /// no-token case gracefully (proceeds without Bearer header). + pub fn health( + &self, + ) -> crate::proto::health::health_client::HealthClient { + crate::proto::health::health_client::HealthClient::with_interceptor( + self.channel.clone(), + self.interceptor.clone(), + ) } /// API Gateway configuration service client (package `foxhunt.config`). @@ -131,10 +182,11 @@ impl FoxhuntClient { /// configuration proto (package `config`). pub fn config_service( &self, - ) -> crate::proto::config_service::configuration_service_client::ConfigurationServiceClient + ) -> crate::proto::config_service::configuration_service_client::ConfigurationServiceClient { - crate::proto::config_service::configuration_service_client::ConfigurationServiceClient::new( + crate::proto::config_service::configuration_service_client::ConfigurationServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } }