fix: ask for a push/TOTP menu choice as text instead of a push confirm

This commit is contained in:
LukeGus committed 2026-10-07 16:40:18 -05:00
1 parent 42c5682243
commit 1466c7bcab
2 files changed
+34 -6

No files matched your search

@@ -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 };
}
@@ -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"] }),