diff --git a/crates/turborepo-auth/src/auth/login.rs b/crates/turborepo-auth/src/auth/login.rs index fa4c966fb70cd..b6de5df21fe77 100644 --- a/crates/turborepo-auth/src/auth/login.rs +++ b/crates/turborepo-auth/src/auth/login.rs @@ -29,6 +29,7 @@ pub async fn login( existing_token, force, sso_team: _, + sso_login_callback_port, } = *options; // Deref or we get double references for each of these // I created a closure that gives back a closure since the `is_valid` checks do @@ -83,7 +84,8 @@ pub async fn login( } } - let redirect_url = format!("http://{DEFAULT_HOST_NAME}:{DEFAULT_PORT}"); + let port = sso_login_callback_port.unwrap_or(DEFAULT_PORT); + let redirect_url = format!("http://{DEFAULT_HOST_NAME}:{port}"); let mut login_url = Url::parse(login_url_configuration)?; let mut success_url = login_url.clone(); success_url @@ -117,7 +119,7 @@ pub async fn login( let token_cell = Arc::new(OnceCell::new()); login_server .run( - DEFAULT_PORT, + port, crate::LoginType::Basic { success_redirect: success_url.to_string(), }, diff --git a/crates/turborepo-auth/src/auth/mod.rs b/crates/turborepo-auth/src/auth/mod.rs index bac8ef2add74c..4005d9b0e9847 100644 --- a/crates/turborepo-auth/src/auth/mod.rs +++ b/crates/turborepo-auth/src/auth/mod.rs @@ -24,6 +24,7 @@ pub struct LoginOptions<'a, T: Client + TokenClient + CacheClient> { pub sso_team: Option<&'a str>, pub existing_token: Option<&'a str>, pub force: bool, + pub sso_login_callback_port: Option, } impl<'a, T: Client + TokenClient + CacheClient> LoginOptions<'a, T> { pub fn new( @@ -40,6 +41,7 @@ impl<'a, T: Client + TokenClient + CacheClient> LoginOptions<'a, T> { sso_team: None, existing_token: None, force: false, + sso_login_callback_port: None, } } } diff --git a/crates/turborepo-auth/src/auth/sso.rs b/crates/turborepo-auth/src/auth/sso.rs index 2d3065e66cb8b..6e5205ee92d4b 100644 --- a/crates/turborepo-auth/src/auth/sso.rs +++ b/crates/turborepo-auth/src/auth/sso.rs @@ -35,6 +35,7 @@ pub async fn sso_login( sso_team, existing_token, force, + sso_login_callback_port, } = *options; let sso_team = sso_team.ok_or(Error::EmptySSOTeam)?; @@ -90,7 +91,8 @@ pub async fn sso_login( } } - let redirect_url = format!("http://{DEFAULT_HOST_NAME}:{DEFAULT_PORT}"); + let port = sso_login_callback_port.unwrap_or(DEFAULT_PORT); + let redirect_url = format!("http://{DEFAULT_HOST_NAME}:{port}"); let mut login_url = Url::parse(login_url_configuration)?; login_url @@ -117,7 +119,7 @@ pub async fn sso_login( let token_cell = Arc::new(OnceCell::new()); login_server - .run(DEFAULT_PORT, crate::LoginType::SSO, token_cell.clone()) + .run(port, crate::LoginType::SSO, token_cell.clone()) .await?; spinner.finish_and_clear(); @@ -384,6 +386,7 @@ mod tests { }; let mut options = LoginOptions { sso_team: Some(team), + sso_login_callback_port: None, ..LoginOptions::new(&color_config, &url, &api_client, &login_server) }; @@ -426,6 +429,7 @@ mod tests { existing_token: None, sso_team: None, force: false, + sso_login_callback_port: None, }; let result = sso_login(&options).await; @@ -450,6 +454,7 @@ mod tests { existing_token: Some("existing-token"), sso_team: Some("my-team"), force: false, + sso_login_callback_port: None, }; let result = sso_login(&options).await.unwrap(); @@ -475,6 +480,7 @@ mod tests { existing_token: Some("existing-token"), sso_team: Some("my-team"), force: true, + sso_login_callback_port: None, }; let result = sso_login(&options).await.unwrap(); diff --git a/crates/turborepo-lib/src/commands/login/manual.rs b/crates/turborepo-lib/src/commands/login/manual.rs index e350921540ca6..0ce1cbf03b86d 100644 --- a/crates/turborepo-lib/src/commands/login/manual.rs +++ b/crates/turborepo-lib/src/commands/login/manual.rs @@ -189,6 +189,7 @@ mod test { upload_timeout: 0, login_url: "".into(), preflight: false, + sso_login_callback_port: None, }; let login_opts = ManualLoginOptions::from(&api_opts); assert_eq!( @@ -212,6 +213,7 @@ mod test { upload_timeout: 0, login_url: "".into(), preflight: false, + sso_login_callback_port: None, }; let login_opts = ManualLoginOptions::from(&api_opts); assert_eq!( diff --git a/crates/turborepo-lib/src/commands/login/mod.rs b/crates/turborepo-lib/src/commands/login/mod.rs index feb6fce214243..cb24467c4fc9d 100644 --- a/crates/turborepo-lib/src/commands/login/mod.rs +++ b/crates/turborepo-lib/src/commands/login/mod.rs @@ -56,10 +56,12 @@ async fn sso_login(base: &mut CommandBase, sso_team: &str, force: bool) -> Resul let api_client: APIClient = base.api_client()?; let color_config = base.color_config; let login_url_config = base.opts.api_client_opts.login_url.to_string(); + let sso_login_callback_port = base.opts.api_client_opts.sso_login_callback_port; let options = LoginOptions { existing_token: base.opts.api_client_opts.token.as_deref(), sso_team: Some(sso_team), force, + sso_login_callback_port, ..LoginOptions::new( &color_config, &login_url_config, @@ -83,10 +85,12 @@ async fn login_no_sso(base: &mut CommandBase, force: bool) -> Result<(), Error> let color_config = base.color_config; let login_url_config = base.opts.api_client_opts.login_url.to_string(); let existing_token = base.opts.api_client_opts.token.as_deref(); + let sso_login_callback_port = base.opts.api_client_opts.sso_login_callback_port; let options = LoginOptions { existing_token, force, + sso_login_callback_port, ..LoginOptions::new( &color_config, &login_url_config, diff --git a/crates/turborepo-lib/src/config/env.rs b/crates/turborepo-lib/src/config/env.rs index c33fd42504a67..28afe8aba1909 100644 --- a/crates/turborepo-lib/src/config/env.rs +++ b/crates/turborepo-lib/src/config/env.rs @@ -45,6 +45,7 @@ const TURBO_MAPPING: &[(&str, &str)] = [ ("turbo_tui_scrollback_length", "tui_scrollback_length"), ("turbo_concurrency", "concurrency"), ("turbo_no_update_notifier", "no_update_notifier"), + ("turbo_sso_login_callback_port", "sso_login_callback_port"), ] .as_slice(); @@ -212,6 +213,14 @@ impl ResolvedConfigurationOptions for EnvVars { .filter(|s| !s.is_empty()) .cloned(); + let sso_login_callback_port = self + .output_map + .get("sso_login_callback_port") + .filter(|s| !s.is_empty()) + .map(|s| s.parse()) + .transpose() + .map_err(Error::InvalidSsoLoginCallbackPort)?; + let output = ConfigurationOptions { api_url: self.output_map.get("api_url").cloned(), login_url: self.output_map.get("login_url").cloned(), @@ -245,6 +254,7 @@ impl ResolvedConfigurationOptions for EnvVars { cache_dir, root_turbo_json_path, log_order, + sso_login_callback_port, }; Ok(output) @@ -339,11 +349,13 @@ mod test { env.insert("turbo_remote_cache_upload_timeout".into(), "200".into()); env.insert("turbo_tui_scrollback_length".into(), "2048".into()); env.insert("turbo_concurrency".into(), "50%".into()); + env.insert("turbo_sso_login_callback_port".into(), "3000".into()); let config = EnvVars::new(&env) .unwrap() .get_configuration_options(&ConfigurationOptions::default()) .unwrap(); + assert_eq!(config.sso_login_callback_port(), Some(3000)); assert!(config.preflight()); assert!(config.force()); assert_eq!(config.log_order(), LogOrder::Grouped); @@ -393,6 +405,7 @@ mod test { env.insert("turbo_allow_no_turbo_json".into(), "".into()); env.insert("turbo_tui_scrollback_length".into(), "".into()); env.insert("turbo_concurrency".into(), "".into()); + env.insert("turbo_sso_login_callback_port".into(), "".into()); let config = EnvVars::new(&env) .unwrap() @@ -421,5 +434,6 @@ mod test { DEFAULT_TUI_SCROLLBACK_LENGTH ); assert_eq!(config.concurrency, None); + assert_eq!(config.sso_login_callback_port(), None); } } diff --git a/crates/turborepo-lib/src/config/mod.rs b/crates/turborepo-lib/src/config/mod.rs index b3a76c58b2eff..a05448adc7c74 100644 --- a/crates/turborepo-lib/src/config/mod.rs +++ b/crates/turborepo-lib/src/config/mod.rs @@ -240,6 +240,8 @@ pub enum Error { scrollback." )] InvalidTuiScrollbackLength(#[source] std::num::ParseIntError), + #[error("TURBO_SSO_LOGIN_CALLBACK_PORT: Invalid value. Use a number for the callback port.")] + InvalidSsoLoginCallbackPort(#[source] std::num::ParseIntError), } const DEFAULT_API_URL: &str = "https://vercel.com/api"; @@ -309,6 +311,7 @@ pub struct ConfigurationOptions { pub(crate) tui_scrollback_length: Option, pub(crate) concurrency: Option, pub(crate) no_update_notifier: Option, + pub(crate) sso_login_callback_port: Option, } #[derive(Default)] @@ -464,6 +467,10 @@ impl ConfigurationOptions { pub fn no_update_notifier(&self) -> bool { self.no_update_notifier.unwrap_or_default() } + + pub fn sso_login_callback_port(&self) -> Option { + self.sso_login_callback_port + } } // Maps Some("") to None to emulate how Go handles empty strings diff --git a/crates/turborepo-lib/src/opts.rs b/crates/turborepo-lib/src/opts.rs index 10dffad49cef4..01a80ac250744 100644 --- a/crates/turborepo-lib/src/opts.rs +++ b/crates/turborepo-lib/src/opts.rs @@ -55,6 +55,7 @@ pub struct APIClientOpts { pub team_slug: Option, pub login_url: String, pub preflight: bool, + pub sso_login_callback_port: Option, } #[derive(Debug, Clone, Serialize)] @@ -449,6 +450,7 @@ impl<'a> From> for APIClientOpts { let team_id = inputs.config.team_id().map(|s| s.to_string()); let team_slug = inputs.config.team_slug().map(|s| s.to_string()); let login_url = inputs.config.login_url().to_string(); + let sso_login_callback_port = inputs.config.sso_login_callback_port(); APIClientOpts { api_url, @@ -459,6 +461,7 @@ impl<'a> From> for APIClientOpts { team_slug, login_url, preflight, + sso_login_callback_port, } } } @@ -737,6 +740,7 @@ mod test { team_slug: None, login_url: "".to_string(), preflight: false, + sso_login_callback_port: None, }, scope_opts, run_opts, diff --git a/docs/site/content/docs/reference/system-environment-variables.mdx b/docs/site/content/docs/reference/system-environment-variables.mdx index bbbf956ce2400..5bd57d28704eb 100644 --- a/docs/site/content/docs/reference/system-environment-variables.mdx +++ b/docs/site/content/docs/reference/system-environment-variables.mdx @@ -340,6 +340,15 @@ System environment variables are always overridden by flag values provided direc settings in run or watch mode. + + + TURBO_SSO_LOGIN_CALLBACK_PORT + + + Override the default port (9789) used for the SSO login callback server + during authentication. + +