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; }