Skip to content

Commit 31ee186

Browse files
nshalmanclaude
andcommitted
triton-cli: prompt for TOTP code on 2FA challenge
Regenerates `triton-gateway-client/src/generated.rs` against the new tritonapi spec (the LoginOutcome enum, LoginVerifyRequest, ChallengeMethod, and the auth_login_verify operation), and teaches `triton login --user <name>` to handle the `LoginOutcome::ChallengeRequired` branch by prompting for an authenticator code (or reading `TRITON_TOTP_CODE` for non-tty flows) and exchanging it via `/v1/auth/login/verify` for the `LoginResponse` the rest of the login pipeline expects. If the server offers only second-factor methods this CLI does not recognise (i.e. all entries reduce to `ChallengeMethod::Unknown`), we refuse before prompting rather than collecting a code we cannot use. SSH-key login is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 115eec6 commit 31ee186

2 files changed

Lines changed: 437 additions & 7 deletions

File tree

cli/triton-cli/src/commands/login.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ use anyhow::{Context, Result, anyhow};
3030
use clap::Args;
3131
use serde::{Deserialize, Serialize};
3232
use triton_gateway_client::TypedClient;
33-
use triton_gateway_client::types::LoginResponse;
33+
use triton_gateway_client::types::{
34+
ChallengeMethod, LoginOutcome, LoginResponse, LoginVerifyRequest,
35+
};
3436

3537
use crate::config::{Profile, paths};
3638

@@ -298,6 +300,59 @@ async fn password_login(client: &TypedClient, username: &str) -> Result<LoginRes
298300
.send()
299301
.await
300302
.map_err(|e| anyhow!("password login failed: {e}"))?;
303+
304+
match response.into_inner() {
305+
LoginOutcome::Complete {
306+
token,
307+
refresh_token,
308+
user,
309+
} => Ok(LoginResponse {
310+
token,
311+
refresh_token,
312+
user,
313+
}),
314+
LoginOutcome::ChallengeRequired {
315+
challenge_token,
316+
methods,
317+
} => prompt_and_verify_totp(client, challenge_token, &methods).await,
318+
}
319+
}
320+
321+
/// Handle a `LoginOutcome::ChallengeRequired`: confirm the server
322+
/// is asking for a method we know how to satisfy, prompt for a
323+
/// TOTP code (or read it from `TRITON_TOTP_CODE` for non-tty
324+
/// flows), and exchange the challenge for a real `LoginResponse`.
325+
async fn prompt_and_verify_totp(
326+
client: &TypedClient,
327+
challenge_token: String,
328+
methods: &[ChallengeMethod],
329+
) -> Result<LoginResponse> {
330+
// v1 only knows TOTP. If the server offers only methods we
331+
// don't support, refuse before prompting -- otherwise we'd
332+
// collect a code we couldn't possibly use.
333+
if !methods.iter().any(|m| matches!(m, ChallengeMethod::Totp)) {
334+
return Err(anyhow!(
335+
"server requires a second-factor method this CLI does not support; \
336+
upgrade `triton` and try again"
337+
));
338+
}
339+
340+
let code = match std::env::var("TRITON_TOTP_CODE").ok() {
341+
Some(c) => c,
342+
None => rpassword::prompt_password("Authenticator code: ")
343+
.map_err(|e| anyhow!("failed to read authenticator code: {e}"))?,
344+
};
345+
let body = LoginVerifyRequest {
346+
challenge_token,
347+
code,
348+
};
349+
let response = client
350+
.inner()
351+
.auth_login_verify()
352+
.body(body)
353+
.send()
354+
.await
355+
.map_err(|e| anyhow!("two-factor verification failed: {e}"))?;
301356
Ok(response.into_inner())
302357
}
303358

0 commit comments

Comments
 (0)