Skip to content

Commit 929323a

Browse files
authored
fix(auth): prevent fallback to device code flow when unified login fails or is cancelled (#868)
* fix auto retry * remove pub
1 parent 0ceb766 commit 929323a

File tree

2 files changed

+65
-77
lines changed

2 files changed

+65
-77
lines changed

crates/chat-cli/src/cli/user.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ impl LoginArgs {
107107
match start_unified_auth(&mut os.database).await? {
108108
PortalResult::Social(provider) => {
109109
pre_portal_spinner.stop_with_message(format!("Logged in with {}", provider));
110-
os.telemetry.send_user_logged_in().ok();
111110
return Ok(ExitCode::SUCCESS);
112111
},
113112
PortalResult::BuilderId { issuer_url, idc_region } => {
@@ -118,10 +117,6 @@ impl LoginArgs {
118117
PortalResult::AwsIdc { issuer_url, idc_region } => {
119118
pre_portal_spinner.stop_with_message("".into());
120119
info!("Completing AWS Identity Center authentication");
121-
// Save IdC credentials for future use
122-
let _ = os.database.set_start_url(issuer_url.clone());
123-
let _ = os.database.set_idc_region(idc_region.clone());
124-
125120
complete_sso_auth(os, issuer_url, idc_region, true).await?;
126121
},
127122
PortalResult::Internal { issuer_url, idc_region } => {
@@ -200,6 +195,8 @@ impl LoginArgs {
200195
}
201196
}
202197

198+
os.telemetry.send_user_logged_in().ok();
199+
203200
Ok(ExitCode::SUCCESS)
204201
}
205202
}
@@ -228,7 +225,9 @@ async fn complete_sso_auth(os: &mut Os, issuer_url: String, idc_region: String,
228225
},
229226
}
230227

231-
os.telemetry.send_user_logged_in().ok();
228+
let _ = os.database.set_start_url(issuer_url.clone());
229+
let _ = os.database.set_idc_region(idc_region.clone());
230+
232231
spinner.stop_with_message("Logged in".into());
233232

234233
// Prompt for profile selection if needed (IdC only)
@@ -416,7 +415,6 @@ async fn try_device_authorization(os: &mut Os, start_url: Option<String>, region
416415
{
417416
PollCreateToken::Pending => {},
418417
PollCreateToken::Complete => {
419-
os.telemetry.send_user_logged_in().ok();
420418
spinner.stop_with_message("Logged in".into());
421419
break;
422420
},

crates/q_cli/src/cli/user.rs

Lines changed: 60 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -273,85 +273,38 @@ pub async fn login_interactive(args: LoginArgs) -> Result<()> {
273273
]);
274274

275275
// Try unified portal, fallback to device flow on any error
276-
let init = match init_unified_portal().await {
277-
Ok(v) => v,
278-
Err(err) => {
279-
pre_portal_spinner.stop();
280-
error!(%err, "Unified portal init failed");
281-
eprintln!("Failed to initialize auth portal.");
282-
eprintln!("Please try again with: q login --use-device-flow");
283-
bail!("Auth portal initialization failed");
284-
},
285-
};
276+
let init = init_unified_portal().await.map_err(|err| {
277+
pre_portal_spinner.stop();
278+
error!(%err, "Unified portal init failed");
279+
eprintln!("Failed to initialize auth portal.\nPlease try again with: kiro-cli login --use-device-flow");
280+
err
281+
})?;
286282

287-
if let Err(err) = fig_util::open_url_async(&init.auth_url).await {
283+
fig_util::open_url_async(&init.auth_url).await.map_err(|err| {
288284
pre_portal_spinner.stop();
289285
error!(%err, "Failed to open portal URL");
290-
eprintln!("Failed to open browser for authentication.");
291-
eprintln!("Please try again with: q login --use-device-flow");
292-
bail!("Failed to open auth portal URL");
293-
}
286+
eprintln!(
287+
"Failed to open browser for authentication.\nPlease try again with: kiro-cli login --use-device-flow"
288+
);
289+
err
290+
})?;
294291

295-
match finish_unified_portal(init, &secret_store).await {
296-
Ok(PortalResult::Social(provider)) => {
292+
match finish_unified_portal(init, &secret_store).await? {
293+
PortalResult::Social(provider) => {
297294
pre_portal_spinner.stop_with_message(format!("Logged in with {}", provider));
298-
fig_telemetry::send_user_logged_in().await;
299-
if let Err(err) = login_command().await {
300-
error!(%err, "Failed to send login command.");
301-
}
302295
Some(AuthMethod::Social(provider))
303296
},
304-
Ok(
305-
PortalResult::Internal { issuer_url, idc_region }
306-
| PortalResult::BuilderId { issuer_url, idc_region }
307-
| PortalResult::AwsIdc { issuer_url, idc_region },
308-
) => {
309-
pre_portal_spinner.stop();
310-
311-
let (client, registration) =
312-
start_pkce_authorization(Some(issuer_url.clone()), Some(idc_region.clone())).await?;
313-
314-
match fig_util::open_url_async(&registration.url).await {
315-
// If it succeeded, finish PKCE.
316-
Ok(()) => {
317-
let mut spinner = Spinner::new(vec![
318-
SpinnerComponent::Spinner,
319-
SpinnerComponent::Text(" Logging in...".into()),
320-
]);
321-
let mut ctrl_c_stream = signal(SignalKind::interrupt())?;
322-
tokio::select! {
323-
res = registration.finish(&client, Some(&secret_store)) => res?,
324-
Some(_) = ctrl_c_stream.recv() => {
325-
#[allow(clippy::exit)]
326-
exit(1);
327-
},
328-
}
329-
fig_telemetry::send_user_logged_in().await;
330-
spinner.stop_with_message("Device authorized".into());
331-
},
332-
// If we are unable to open the link with the browser, then fallback to
333-
// the device code flow.
334-
Err(err) => {
335-
error!(%err, "Failed to open URL with browser, falling back to device code flow");
336-
337-
// Try device code flow.
338-
try_device_authorization(&secret_store, Some(issuer_url), Some(idc_region)).await?;
339-
},
340-
}
297+
PortalResult::BuilderId { issuer_url, idc_region } => {
298+
pre_portal_spinner.stop_with_message("".into());
299+
complete_sso_auth(&secret_store, issuer_url, idc_region).await?;
300+
Some(AuthMethod::BuilderId)
301+
},
341302

303+
PortalResult::Internal { issuer_url, idc_region } | PortalResult::AwsIdc { issuer_url, idc_region } => {
304+
pre_portal_spinner.stop_with_message("".into());
305+
complete_sso_auth(&secret_store, issuer_url, idc_region).await?;
342306
Some(AuthMethod::IdentityCenter)
343307
},
344-
Err(err) => {
345-
pre_portal_spinner.stop();
346-
error!(%err, "Unified portal failed, falling back to device code flow");
347-
try_device_authorization(&secret_store, None, None).await?;
348-
fig_telemetry::send_user_logged_in().await;
349-
if let Err(err) = login_command().await {
350-
error!(%err, "Failed to send login command.");
351-
}
352-
eprintln!("Logged in successfully");
353-
return Ok(());
354-
},
355308
}
356309
} else {
357310
// Remote environment: Use device flow only
@@ -415,6 +368,8 @@ pub async fn login_interactive(args: LoginArgs) -> Result<()> {
415368
Some(method)
416369
};
417370

371+
fig_telemetry::send_user_logged_in().await;
372+
418373
if let Err(err) = login_command().await {
419374
error!(%err, "Failed to send login command.");
420375
}
@@ -474,7 +429,6 @@ async fn try_device_authorization(
474429
{
475430
PollCreateToken::Pending => {},
476431
PollCreateToken::Complete(_) => {
477-
fig_telemetry::send_user_logged_in().await;
478432
spinner.stop_with_message("Device authorized".into());
479433
break;
480434
},
@@ -487,6 +441,42 @@ async fn try_device_authorization(
487441
Ok(())
488442
}
489443

444+
/// Complete SSO authentication (BuilderID, IdC, or Internal) after portal selection
445+
async fn complete_sso_auth(secret_store: &SecretStore, issuer_url: String, idc_region: String) -> Result<()> {
446+
let (client, registration) = start_pkce_authorization(Some(issuer_url.clone()), Some(idc_region.clone())).await?;
447+
448+
match fig_util::open_url_async(&registration.url).await {
449+
Ok(()) => {
450+
// Browser opened successfully, wait for PKCE flow to complete
451+
let mut spinner = Spinner::new(vec![
452+
SpinnerComponent::Spinner,
453+
SpinnerComponent::Text(" Logging in...".into()),
454+
]);
455+
456+
let mut ctrl_c_stream = signal(SignalKind::interrupt())?;
457+
tokio::select! {
458+
res = registration.finish(&client, Some(secret_store)) => res?,
459+
Some(_) = ctrl_c_stream.recv() => {
460+
#[allow(clippy::exit)]
461+
exit(1);
462+
},
463+
}
464+
465+
let _ = fig_settings::state::set_value("auth.idc.start-url", issuer_url.clone());
466+
let _ = fig_settings::state::set_value("auth.idc.region", idc_region.clone());
467+
468+
spinner.stop_with_message("Device authorized".into());
469+
},
470+
Err(err) => {
471+
// Failed to open browser, fallback to device code flow
472+
error!(%err, "Failed to open URL, falling back to device code flow");
473+
try_device_authorization(secret_store, Some(issuer_url), Some(idc_region)).await?;
474+
},
475+
}
476+
477+
Ok(())
478+
}
479+
490480
async fn select_profile_interactive(whoami: bool) -> Result<()> {
491481
let mut spinner = Spinner::new(vec![
492482
SpinnerComponent::Spinner,

0 commit comments

Comments
 (0)