Skip to content

Commit 4a5210e

Browse files
fix: use dynamic port for Tetrate auth callback server (#7228)
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
1 parent 9b6669a commit 4a5210e

File tree

4 files changed

+35
-43
lines changed

4 files changed

+35
-43
lines changed

crates/goose/examples/tetrate_auth.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1010
// Create new PKCE auth flow
1111
let mut auth_flow = TetrateAuth::new()?;
1212

13-
// Get the auth URL that would be opened
14-
let auth_url = auth_flow.get_auth_url();
15-
println!("Auth URL: {}", auth_url);
16-
println!("\nStarting authentication flow...");
13+
println!("Starting authentication flow...");
1714
println!("This will:");
18-
println!("1. Open your browser to the auth page");
19-
println!("2. Start a local server on port 3000");
15+
println!("1. Start a local server on a dynamic port");
16+
println!("2. Open your browser to the auth page");
2017
println!("3. Wait for the callback\n");
2118

2219
// Complete the full flow

crates/goose/src/config/signup_tetrate/mod.rs

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub const TETRATE_DEFAULT_MODEL: &str = "claude-haiku-4-5";
1919
// Auth endpoints are on the main web domain
2020
const TETRATE_AUTH_URL: &str = "https://router.tetrate.ai/auth";
2121
const TETRATE_TOKEN_URL: &str = "https://router.tetrate.ai/api/api-keys/verify";
22-
const CALLBACK_URL: &str = "http://localhost:3000";
22+
const CALLBACK_BASE: &str = "http://localhost";
2323
const AUTH_TIMEOUT: Duration = Duration::from_secs(180); // 3 minutes
2424

2525
#[derive(Debug)]
@@ -61,38 +61,16 @@ impl PkceAuthFlow {
6161
})
6262
}
6363

64-
pub fn get_auth_url(&self) -> String {
64+
pub fn get_auth_url(&self, port: u16) -> String {
65+
let callback_url = format!("{}:{}", CALLBACK_BASE, port);
6566
format!(
6667
"{}?callback={}&code_challenge={}",
6768
TETRATE_AUTH_URL,
68-
urlencoding::encode(CALLBACK_URL),
69+
urlencoding::encode(&callback_url),
6970
urlencoding::encode(&self.code_challenge)
7071
)
7172
}
7273

73-
/// Start local server and wait for callback
74-
pub async fn start_server(&mut self) -> Result<String> {
75-
let (code_tx, code_rx) = oneshot::channel::<String>();
76-
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
77-
78-
// Store shutdown sender so we can stop the server later
79-
self.server_shutdown_tx = Some(shutdown_tx);
80-
81-
// Start the server in a background task
82-
tokio::spawn(async move {
83-
if let Err(e) = server::run_callback_server(code_tx, shutdown_rx).await {
84-
eprintln!("Server error: {}", e);
85-
}
86-
});
87-
88-
// Wait for the authorization code with timeout
89-
match timeout(AUTH_TIMEOUT, code_rx).await {
90-
Ok(Ok(code)) => Ok(code),
91-
Ok(Err(_)) => Err(anyhow!("Failed to receive authorization code")),
92-
Err(_) => Err(anyhow!("Authentication timeout - please try again")),
93-
}
94-
}
95-
9674
pub async fn exchange_code(&self, code: String) -> Result<String> {
9775
let client = Client::new();
9876

@@ -131,9 +109,22 @@ impl PkceAuthFlow {
131109
Ok(token_response.key)
132110
}
133111

134-
/// Complete flow: open browser, wait for callback, exchange code
112+
/// Complete flow: start server, open browser, wait for callback, exchange code
135113
pub async fn complete_flow(&mut self) -> Result<String> {
136-
let auth_url = self.get_auth_url();
114+
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)).await?;
115+
let port = listener.local_addr()?.port();
116+
117+
let (code_tx, code_rx) = oneshot::channel::<String>();
118+
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
119+
self.server_shutdown_tx = Some(shutdown_tx);
120+
121+
tokio::spawn(async move {
122+
if let Err(e) = server::run_callback_server(listener, code_tx, shutdown_rx).await {
123+
eprintln!("Server error: {}", e);
124+
}
125+
});
126+
127+
let auth_url = self.get_auth_url(port);
137128

138129
println!("Opening browser for Tetrate Agent Router Service authentication...");
139130
eprintln!("Auth URL: {}", auth_url);
@@ -143,8 +134,13 @@ impl PkceAuthFlow {
143134
println!("Please open this URL manually: {}", auth_url);
144135
}
145136

146-
println!("Waiting for authentication callback...");
147-
let code = self.start_server().await?;
137+
println!("Waiting for authentication callback on port {}...", port);
138+
139+
let code = match timeout(AUTH_TIMEOUT, code_rx).await {
140+
Ok(Ok(code)) => Ok(code),
141+
Ok(Err(_)) => Err(anyhow!("Failed to receive authorization code")),
142+
Err(_) => Err(anyhow!("Authentication timeout - please try again")),
143+
}?;
148144

149145
println!("Authorization code received. Exchanging for API key...");
150146
eprintln!("Received code: {}", code);

crates/goose/src/config/signup_tetrate/server.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use axum::{
99
use include_dir::{include_dir, Dir};
1010
use minijinja::{context, Environment};
1111
use serde::Deserialize;
12-
use std::net::SocketAddr;
1312
use tokio::sync::oneshot;
1413

1514
static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/config/signup_tetrate/templates");
@@ -20,14 +19,13 @@ struct CallbackQuery {
2019
error: Option<String>,
2120
}
2221

23-
/// Run the callback server on localhost:3000
22+
/// Run the callback server using the provided listener.
2423
pub async fn run_callback_server(
24+
listener: tokio::net::TcpListener,
2525
code_tx: oneshot::Sender<String>,
2626
shutdown_rx: oneshot::Receiver<()>,
2727
) -> Result<()> {
2828
let app = Router::new().route("/", get(handle_callback));
29-
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
30-
let listener = tokio::net::TcpListener::bind(addr).await?;
3129
let state = std::sync::Arc::new(tokio::sync::Mutex::new(Some(code_tx)));
3230

3331
axum::serve(listener, app.with_state(state.clone()).into_make_service())

crates/goose/src/config/signup_tetrate/tests.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,16 @@ fn test_code_challenge_generation() {
3434
#[test]
3535
fn test_auth_url_generation() {
3636
let flow = PkceAuthFlow::new().unwrap();
37-
let auth_url = flow.get_auth_url();
37+
let auth_url = flow.get_auth_url(12345);
3838

3939
// Verify URL contains required parameters
4040
assert!(auth_url.contains("callback="));
4141
assert!(auth_url.contains("code_challenge="));
4242
assert!(auth_url.starts_with(TETRATE_AUTH_URL));
4343

44-
// Verify callback URL is properly encoded
45-
assert!(auth_url.contains(&*urlencoding::encode(CALLBACK_URL)));
44+
// Verify callback URL contains the dynamic port
45+
let expected_callback = format!("{}:{}", CALLBACK_BASE, 12345);
46+
assert!(auth_url.contains(&*urlencoding::encode(&expected_callback)));
4647
}
4748

4849
#[test]

0 commit comments

Comments
 (0)