mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 15:45:23 +00:00
feat: 2FA With FortiToken (#1288)
https://github.com/Termix-SSH/Support/issues/1288
This commit is contained in:
1 parent
42e7861073
commit
6ce5e78165
4 files changed
+65
-1
No files matched your search
@@ -123,6 +123,16 @@ export class SSHAuthManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FortiToken-style prompt: asks the user to type a code OR the literal
|
||||||
|
// word "push" (e.g. "Enter your Token or type 'push' to receive a push
|
||||||
|
// notification:"). Unlike JumpCloud/DUO's confirm-only push, this needs a
|
||||||
|
// text field, so it must not fall into the empty-answerable push pattern
|
||||||
|
// below. Checked first since its text also contains "push notification".
|
||||||
|
const fortiTokenPattern = /type\s+['"]?push['"]?/i;
|
||||||
|
const isFortiTokenPrompt = promptTexts.some((p) =>
|
||||||
|
fortiTokenPattern.test(p),
|
||||||
|
);
|
||||||
|
|
||||||
// JumpCloud Protect / DUO-style push MFA: a menu choice ("Choose [1] Push,
|
// JumpCloud Protect / DUO-style push MFA: a menu choice ("Choose [1] Push,
|
||||||
// or [2] TOTP:") followed by an empty-answerable confirm ("Press enter to
|
// or [2] TOTP:") followed by an empty-answerable confirm ("Press enter to
|
||||||
// send Push request:"). Checked before the TOTP regex because the menu
|
// send Push request:"). Checked before the TOTP regex because the menu
|
||||||
@@ -130,7 +140,8 @@ export class SSHAuthManager {
|
|||||||
// misrouted into the numeric-code flow.
|
// misrouted into the numeric-code flow.
|
||||||
const pushPromptPattern =
|
const pushPromptPattern =
|
||||||
/choose.*push.*totp|press enter.*(push|send)|push notification|authentication by phone/i;
|
/choose.*push.*totp|press enter.*(push|send)|push notification|authentication by phone/i;
|
||||||
const isPushPrompt = promptTexts.some((p) => pushPromptPattern.test(p));
|
const isPushPrompt =
|
||||||
|
!isFortiTokenPrompt && promptTexts.some((p) => pushPromptPattern.test(p));
|
||||||
|
|
||||||
if (isPushPrompt) {
|
if (isPushPrompt) {
|
||||||
sshLogger.info("Push/menu MFA prompt detected", {
|
sshLogger.info("Push/menu MFA prompt detected", {
|
||||||
|
|||||||
@@ -170,6 +170,46 @@ describe("SSHAuthManager.handleKeyboardInteractive", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes a FortiToken prompt to the totp flow instead of the push-confirm flow", () => {
|
||||||
|
const { manager, sent } = createManager();
|
||||||
|
const finish = vi.fn();
|
||||||
|
|
||||||
|
manager.handleKeyboardInteractive(
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
prompt:
|
||||||
|
"Enter your Token or type 'push' to receive a push notification: ",
|
||||||
|
echo: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
finish,
|
||||||
|
{ username: "root", authType: "none" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sent).toEqual([
|
||||||
|
{
|
||||||
|
type: "connection_log",
|
||||||
|
data: {
|
||||||
|
stage: "auth",
|
||||||
|
level: "info",
|
||||||
|
message: "TOTP verification required",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "totp_required",
|
||||||
|
prompt:
|
||||||
|
"Enter your Token or type 'push' to receive a push notification: ",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
manager.context.keyboardInteractiveFinish?.(["push"]);
|
||||||
|
|
||||||
|
expect(finish).toHaveBeenCalledWith(["push"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("routes Warpgate prompts to the warpgate flow, not the generic path", () => {
|
it("routes Warpgate prompts to the warpgate flow, not the generic path", () => {
|
||||||
const { manager, sent } = createManager();
|
const { manager, sent } = createManager();
|
||||||
const finish = vi.fn();
|
const finish = vi.fn();
|
||||||
|
|||||||
@@ -2182,6 +2182,7 @@
|
|||||||
"mfaPromptRequired": "Authentication Required",
|
"mfaPromptRequired": "Authentication Required",
|
||||||
"mfaPushRequired": "Push Authentication Required",
|
"mfaPushRequired": "Push Authentication Required",
|
||||||
"mfaMenuPlaceholder": "Enter your response",
|
"mfaMenuPlaceholder": "Enter your response",
|
||||||
|
"mfaCodeOrPushPlaceholder": "Code or \"push\"",
|
||||||
"mfaWaitingApproval": "Waiting for approval on your device...",
|
"mfaWaitingApproval": "Waiting for approval on your device...",
|
||||||
"mfaSendRequest": "Send Request",
|
"mfaSendRequest": "Send Request",
|
||||||
"warpgateAuthRequired": "Warpgate Authentication Required",
|
"warpgateAuthRequired": "Warpgate Authentication Required",
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ export function TOTPDialog({
|
|||||||
const isMenu = mode === "menu";
|
const isMenu = mode === "menu";
|
||||||
const isTotp = mode === "totp";
|
const isTotp = mode === "totp";
|
||||||
const showWaiting = waiting || (isPush && pushSubmitted);
|
const showWaiting = waiting || (isPush && pushSubmitted);
|
||||||
|
// FortiToken-style prompts ask for a numeric code or the literal word
|
||||||
|
// "push", so the field can't be restricted to digits only.
|
||||||
|
const allowsPushKeyword = isTotp && /push/i.test(prompt);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -114,6 +117,15 @@ export function TOTPDialog({
|
|||||||
placeholder={t("terminal.mfaMenuPlaceholder")}
|
placeholder={t("terminal.mfaMenuPlaceholder")}
|
||||||
className="rounded-none bg-muted/50 border-border text-center text-sm tracking-widest"
|
className="rounded-none bg-muted/50 border-border text-center text-sm tracking-widest"
|
||||||
/>
|
/>
|
||||||
|
) : allowsPushKeyword ? (
|
||||||
|
<Input
|
||||||
|
id="totpCode"
|
||||||
|
name="totpCode"
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
placeholder={t("terminal.mfaCodeOrPushPlaceholder")}
|
||||||
|
className="rounded-none bg-muted/50 border-border text-center text-sm tracking-widest"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
id="totpCode"
|
id="totpCode"
|
||||||
|
|||||||
Reference in new issue
Block a user