From 1466c7bcabf6ab9dfb6354acfdb0c8897c1e53d2 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 7 Oct 2026 16:40:18 -0500 Subject: [PATCH] fix: ask for a push/TOTP menu choice as text instead of a push confirm --- .../hosts/connect/keyboard-interactive.ts | 16 ++++++++----- .../tests/plugins/ctx-ssh-auth.test.ts | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/backend/hosts/connect/keyboard-interactive.ts b/src/backend/hosts/connect/keyboard-interactive.ts index 02cb68c8a..12030c4cf 100644 --- a/src/backend/hosts/connect/keyboard-interactive.ts +++ b/src/backend/hosts/connect/keyboard-interactive.ts @@ -21,10 +21,13 @@ const PASSWORD_PATTERN = /password/i; // because its text also says "push notification". const FORTI_TOKEN_PATTERN = /type\s+['"]?push['"]?/i; -// JumpCloud Protect / DUO: a menu choice, then an empty-answerable confirm. -// Checked before TOTP because "...or [2] TOTP:" would match that pattern. +// JumpCloud Protect / DUO: a menu choice answered with a number, then an +// empty-answerable confirm. Checked before TOTP because "...or [2] TOTP:" +// would match that pattern. The menu needs a text field, so it is not a push. +const PUSH_MENU_PATTERN = /choose.*push.*totp/i; + const PUSH_PROMPT_PATTERN = - /choose.*push.*totp|press enter.*(push|send)|push notification|authentication by phone/i; + /press enter.*(push|send)|push notification|authentication by phone/i; const TOTP_PATTERN = /verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i; @@ -77,10 +80,11 @@ export function classifyKeyboardInteractive( const { prompts } = round; const texts = prompts.map((p) => p.prompt); const isFortiToken = texts.some((p) => FORTI_TOKEN_PATTERN.test(p)); - const isPush = - !isFortiToken && texts.some((p) => PUSH_PROMPT_PATTERN.test(p)); + const isPushFlow = + !isFortiToken && + texts.some((p) => PUSH_MENU_PATTERN.test(p) || PUSH_PROMPT_PATTERN.test(p)); - if (!isPush) { + if (!isPushFlow) { const totpIndex = prompts.findIndex((p) => TOTP_PATTERN.test(p.prompt)); if (totpIndex !== -1) return { kind: "totp", promptIndex: totpIndex }; } diff --git a/src/backend/tests/plugins/ctx-ssh-auth.test.ts b/src/backend/tests/plugins/ctx-ssh-auth.test.ts index 3d701ac0f..205a47c62 100644 --- a/src/backend/tests/plugins/ctx-ssh-auth.test.ts +++ b/src/backend/tests/plugins/ctx-ssh-auth.test.ts @@ -556,6 +556,30 @@ describe("ctx.auth", () => { ).not.toBe("browser"); }); + it("asks for a push/TOTP menu choice as text and a press-enter confirm as a push", () => { + const host = { id: 1, ip: "10.0.0.1", port: 22, username: "root" }; + expect( + classifyKeyboardInteractive( + { + name: "", + instructions: "", + prompts: [{ prompt: "Choose [1] Push, or [2] TOTP: ", echo: true }], + }, + host, + ), + ).toEqual({ kind: "input", promptIndex: 0, isPush: false }); + expect( + classifyKeyboardInteractive( + { + name: "", + instructions: "", + prompts: [{ prompt: "Press enter to send push", echo: false }], + }, + host, + ), + ).toEqual({ kind: "input", promptIndex: 0, isPush: true }); + }); + it("refuses a keyboard-interactive handler that is not declared", () => { const auth = createPluginAuth({ manifest: manifest(["auth:provide"], { keyboardInteractive: ["gate"] }),