From 9fd7b6a8573c64939f848e19d14af137d5c248a5 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 10 Jun 2026 11:05:36 -0700 Subject: [PATCH] Green: prefer TOTP via twoFactorSessionIDV2 when passkey also enrolled beginLogin now checks twoFactorSessionID ?? twoFactorSessionIDV2 before the passkey branch. Accounts with both passkeys and TOTP can now log in from the CLI using their authenticator app. --- src/auth/login.ts | 10 ++++++++-- src/auth/types.ts | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/auth/login.ts b/src/auth/login.ts index 5a53f42..4123b1b 100644 --- a/src/auth/login.ts +++ b/src/auth/login.ts @@ -73,8 +73,14 @@ const srpHandshake = async ( srpClient.checkM2(Buffer.from(verifyResponse.srpM2, "base64")); - if (verifyResponse.twoFactorSessionID) { - return { kind: "totp", sessionID: verifyResponse.twoFactorSessionID }; + // twoFactorSessionIDV2 is set (instead of twoFactorSessionID) when the + // account has BOTH passkeys and TOTP. Prefer TOTP: a CLI cannot perform + // a WebAuthn ceremony, and the user has a TOTP secret enrolled. + const totpSessionID = + verifyResponse.twoFactorSessionID ?? + verifyResponse.twoFactorSessionIDV2; + if (totpSessionID) { + return { kind: "totp", sessionID: totpSessionID }; } if (verifyResponse.passkeySessionID) { return { diff --git a/src/auth/types.ts b/src/auth/types.ts index e6a23f4..5fd71e7 100644 --- a/src/auth/types.ts +++ b/src/auth/types.ts @@ -34,13 +34,18 @@ export interface SRPAttributes { // The body of a successful login response. Exactly one of the following // situations applies, and the caller dispatches on the populated fields: // - both keyAttributes and encryptedToken present: login is complete -// - twoFactorSessionID present: caller must submit a TOTP code -// - passkeySessionID present: caller must complete a passkey ceremony +// - twoFactorSessionID present: caller must submit a TOTP code (TOTP-only +// account) +// - passkeySessionID + twoFactorSessionIDV2 present: account has both +// passkeys and TOTP; the V2 field is set instead of twoFactorSessionID +// so that older clients keep using the passkey flow +// - only passkeySessionID present: caller must complete a passkey ceremony export interface AuthorizationResponse { id: number; keyAttributes?: KeyAttributes; encryptedToken?: Base64; twoFactorSessionID?: string; + twoFactorSessionIDV2?: string; passkeySessionID?: string; }