1) Сделать отдельный эндпонт для начала аутентификации типа authflow/usernamepassword
ответ:
{
"nextStep": "email-code" | "totp-code" | "done",
"token": "xxx" | null, // auth token, session id или что у вас используется
"state": "xxx", // unique id для stateful приложений, либо нечто зашифрованное для распределенных stateless
}
2) Добавить эндпоинты для продолжения аутентификации типа authflow/email-code, authflow/totp-code итд
ответ тот же что и в п.1
на беке после проверки username/password проверять если ли у юзера MFA. Если есть говорить что следующий шаг это этот фактор, если нет сразу результат логина (токен).
если вход сделан через oauth2 то в конце можно вернуть authorization code и уже его обменять на токен.
для кодов использовать TOTP либо HOTP, с первым вариантом ничего в БД сохранять не надо, во втором надо.
btw. JWT тут не обязателен, можно и без него сделать.