diff --git a/autotests/rocketchatmessagetest.h b/autotests/rocketchatmessagetest.h --- a/autotests/rocketchatmessagetest.h +++ b/autotests/rocketchatmessagetest.h @@ -80,6 +80,7 @@ void shouldAddUserToRoom(); void shouldLogin(); + void shouldLoginCode(); void inputChannelAutocomplete(); diff --git a/autotests/rocketchatmessagetest.cpp b/autotests/rocketchatmessagetest.cpp --- a/autotests/rocketchatmessagetest.cpp +++ b/autotests/rocketchatmessagetest.cpp @@ -395,10 +395,18 @@ { RocketChatMessage m; m.setJsonFormat(QJsonDocument::Indented); - RocketChatMessage::RocketChatMessageResult r = m.login(QStringLiteral("user"), QStringLiteral("password"), 43); + RocketChatMessage::RocketChatMessageResult r = m.login(QStringLiteral("user"), QStringLiteral("password"), QString(), 43); compareFile(r.result, QStringLiteral("login")); } +void RocketChatMessageTest::shouldLoginCode() +{ + RocketChatMessage m; + m.setJsonFormat(QJsonDocument::Indented); + RocketChatMessage::RocketChatMessageResult r = m.login(QStringLiteral("user"), QStringLiteral("password"), QStringLiteral("1234"), 43); + compareFile(r.result, QStringLiteral("loginCode")); +} + void RocketChatMessageTest::shouldLoginProvider() { RocketChatMessage m; diff --git a/src/apps/qml/Login.qml b/src/apps/qml/Login.qml --- a/src/apps/qml/Login.qml +++ b/src/apps/qml/Login.qml @@ -35,6 +35,7 @@ property QtObject rcAccount property alias username: usernameField.text; property alias password: passField.text; + property alias twoFactorAuthenticationCode: twoFactorAuthenticationCodeField.text; property alias serverUrl: urlField.text; property alias accountName: nameField.text; @@ -161,6 +162,29 @@ } } + QQC2.Label { + id: twoFactorAuthenticationCodeLabel + + width: parent.width + wrapMode: Text.Wrap + text: i18n("You have enabled second factor authentication. Please enter the generated code or a backup code.") + color: Kirigami.Theme.negativeTextColor + font.bold: true + visible: rcAccount.loginStatus === DDPClient.LoginCodeRequired + } + + PasswordLineEdit { + id: twoFactorAuthenticationCodeField + width: parent.width + placeholderText: i18n("Two-factor authentication code or backup code") + visible: rcAccount.loginStatus === DDPClient.LoginCodeRequired + onAccepted: { + if (acceptingButton.enabled) { + acceptingButton.clicked(); + } + } + } + Item { id: spacer2 width: 30 diff --git a/src/apps/qml/LoginPage.qml b/src/apps/qml/LoginPage.qml --- a/src/apps/qml/LoginPage.qml +++ b/src/apps/qml/LoginPage.qml @@ -35,11 +35,13 @@ username: rcAccount.userName originalAccountName: rcAccount.accountName password: rcAccount.password + twoFactorAuthenticationCode: rcAccount.twoFactorAuthenticationCode onAccepted: { //TODO ? //rcAccount.updateAccountSettings(loginTab.accountName, loginTab.password, loginTab.username, loginTab.serverUrl) rcAccount.accountName = loginTab.accountName; rcAccount.password = loginTab.password; + rcAccount.twoFactorAuthenticationCode = loginTab.twoFactorAuthenticationCode; rcAccount.userName = loginTab.username; rcAccount.serverUrl = loginTab.serverUrl; rcAccount.tryLogin(); diff --git a/src/ruqolacore/ddpapi/ddpclient.h b/src/ruqolacore/ddpapi/ddpclient.h --- a/src/ruqolacore/ddpapi/ddpclient.h +++ b/src/ruqolacore/ddpapi/ddpclient.h @@ -43,6 +43,7 @@ LoggingIn, LoggedIn, LoginFailed, + LoginCodeRequired, LoggedOut, FailedToLoginPluginProblem }; diff --git a/src/ruqolacore/ddpapi/ddpclient.cpp b/src/ruqolacore/ddpapi/ddpclient.cpp --- a/src/ruqolacore/ddpapi/ddpclient.cpp +++ b/src/ruqolacore/ddpapi/ddpclient.cpp @@ -670,7 +670,7 @@ quint64 DDPClient::login(const QString &username, const QString &password) { - const RocketChatMessage::RocketChatMessageResult result = mRocketChatMessage->login(username, password, m_uid); + const RocketChatMessage::RocketChatMessageResult result = mRocketChatMessage->login(username, password, mRocketChatAccount->settings()->twoFactorAuthenticationCode(), m_uid); return method(result, login_result, DDPClient::Ephemeral); } @@ -827,10 +827,18 @@ Q_EMIT result(id, QJsonDocument(root.value(QLatin1String("result")).toObject())); if (id == m_loginJob) { - if (root.value(QLatin1String("error")).toObject().value(QLatin1String("error")).toInt() == 403) { + const QJsonObject error(root.value(QLatin1String("error")).toObject()); + const QJsonValue errorValue(error.value(QLatin1String("error"))); + if (errorValue.toInt() == 403) { qCDebug(RUQOLA_DDPAPI_LOG) << "Wrong password or token expired"; login(); // Let's keep trying to log in + } else if (errorValue.toString() == QLatin1String("totp-required")) { + qCDebug(RUQOLA_DDPAPI_LOG) << "A 2FA code or backup code is required to login"; + setLoginStatus(LoginCodeRequired); + } else if (!error.isEmpty()) { + qCDebug(RUQOLA_DDPAPI_LOG) << error.value(QLatin1String("message")).toString(); + setLoginStatus(LoginFailed); } else { const QString token = root.value(QLatin1String("result")).toObject().value(QLatin1String("token")).toString(); mRocketChatAccount->settings()->setAuthToken(token); diff --git a/src/ruqolacore/rocketchataccount.h b/src/ruqolacore/rocketchataccount.h --- a/src/ruqolacore/rocketchataccount.h +++ b/src/ruqolacore/rocketchataccount.h @@ -79,6 +79,7 @@ Q_PROPERTY(QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY serverUrlChanged) Q_PROPERTY(QString accountName READ accountName WRITE setAccountName NOTIFY accountNameChanged) Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged) + Q_PROPERTY(QString twoFactorAuthenticationCode READ twoFactorAuthenticationCode WRITE setTwoFactorAuthenticationCode NOTIFY twoFactorAuthenticationCodeChanged) Q_PROPERTY(DDPClient::LoginStatus loginStatus READ loginStatus NOTIFY loginStatusChanged) Q_PROPERTY(bool editingMode READ editingMode NOTIFY editingModeChanged) Q_PROPERTY(bool sortUnreadOnTop READ sortUnreadOnTop NOTIFY sortUnreadOnTopChanged) @@ -280,6 +281,9 @@ void setPassword(const QString &password); Q_REQUIRED_RESULT QString password() const; + void setTwoFactorAuthenticationCode(const QString &twoFactorAuthenticationCode); + Q_REQUIRED_RESULT QString twoFactorAuthenticationCode() const; + void setAuthToken(const QString &token); Q_REQUIRED_RESULT QString authToken() const; @@ -340,6 +344,7 @@ void userNameChanged(); void userIDChanged(); void passwordChanged(); + void twoFactorAuthenticationCodeChanged(); void serverUrlChanged(); void loginStatusChanged(); void logoutDone(const QString &accountname); diff --git a/src/ruqolacore/rocketchataccount.cpp b/src/ruqolacore/rocketchataccount.cpp --- a/src/ruqolacore/rocketchataccount.cpp +++ b/src/ruqolacore/rocketchataccount.cpp @@ -1213,6 +1213,11 @@ return settings()->password(); } +QString RocketChatAccount::twoFactorAuthenticationCode() const +{ + return settings()->twoFactorAuthenticationCode(); +} + void RocketChatAccount::setAuthToken(const QString &token) { settings()->setAuthToken(token); @@ -1223,6 +1228,11 @@ settings()->setPassword(password); } +void RocketChatAccount::setTwoFactorAuthenticationCode(const QString &twoFactorAuthenticationCode) +{ + settings()->setTwoFactorAuthenticationCode(twoFactorAuthenticationCode); +} + void RocketChatAccount::setUserName(const QString &username) { settings()->setUserName(username); diff --git a/src/ruqolacore/rocketchataccountsettings.h b/src/ruqolacore/rocketchataccountsettings.h --- a/src/ruqolacore/rocketchataccountsettings.h +++ b/src/ruqolacore/rocketchataccountsettings.h @@ -57,6 +57,9 @@ Q_REQUIRED_RESULT QString password() const; void setPassword(const QString &password); + Q_REQUIRED_RESULT QString twoFactorAuthenticationCode() const; + void setTwoFactorAuthenticationCode(const QString &twoFactorAuthenticationCode); + void removeSettings(); Q_REQUIRED_RESULT bool showUnreadOnTop() const; @@ -68,6 +71,7 @@ void userIDChanged(); void accountNameChanged(); void passwordChanged(); + void twoFactorAuthenticationCodeChanged(); private: Q_DISABLE_COPY(RocketChatAccountSettings) @@ -82,6 +86,7 @@ QString mCachePath; QString mUserName; QString mPassword; + QString mTwoFactorAuthenticationCode; QSettings *mSetting = nullptr; bool mShowUnreadOnTop = false; }; diff --git a/src/ruqolacore/rocketchataccountsettings.cpp b/src/ruqolacore/rocketchataccountsettings.cpp --- a/src/ruqolacore/rocketchataccountsettings.cpp +++ b/src/ruqolacore/rocketchataccountsettings.cpp @@ -161,6 +161,20 @@ Q_EMIT passwordChanged(); } +QString RocketChatAccountSettings::twoFactorAuthenticationCode() const +{ + return mTwoFactorAuthenticationCode; +} + +void RocketChatAccountSettings::setTwoFactorAuthenticationCode(const QString &twoFactorAuthenticationCode) +{ + if (mTwoFactorAuthenticationCode != twoFactorAuthenticationCode) { + mTwoFactorAuthenticationCode = twoFactorAuthenticationCode; + + Q_EMIT twoFactorAuthenticationCodeChanged(); + } +} + QString RocketChatAccountSettings::userName() const { return mUserName; diff --git a/src/ruqolacore/rocketchatmessage.h b/src/ruqolacore/rocketchatmessage.h --- a/src/ruqolacore/rocketchatmessage.h +++ b/src/ruqolacore/rocketchatmessage.h @@ -91,7 +91,7 @@ Q_REQUIRED_RESULT RocketChatMessage::RocketChatMessageResult roomFiles(const QString &roomId, quint64 id); Q_REQUIRED_RESULT RocketChatMessage::RocketChatMessageResult searchRoomUsers(const QString &pattern, const QString &exceptions, bool searchUser, bool searchRoom, quint64 id); Q_REQUIRED_RESULT RocketChatMessage::RocketChatMessageResult addUserToRoom(const QString &username, const QString &roomId, quint64 id); - Q_REQUIRED_RESULT RocketChatMessage::RocketChatMessageResult login(const QString &username, const QString &password, quint64 id); + Q_REQUIRED_RESULT RocketChatMessage::RocketChatMessageResult login(const QString &username, const QString &password, const QString &twoFactorAuthenticationCode, quint64 id); Q_REQUIRED_RESULT RocketChatMessage::RocketChatMessageResult inputChannelAutocomplete(const QString &pattern, const QString &exceptions, quint64 id); Q_REQUIRED_RESULT RocketChatMessage::RocketChatMessageResult inputUserAutocomplete(const QString &pattern, const QString &exceptions, quint64 id); Q_REQUIRED_RESULT RocketChatMessage::RocketChatMessageResult loginProvider(const QString &credentialToken, const QString &credentialSecretd, quint64 id); diff --git a/src/ruqolacore/rocketchatmessage.cpp b/src/ruqolacore/rocketchatmessage.cpp --- a/src/ruqolacore/rocketchatmessage.cpp +++ b/src/ruqolacore/rocketchatmessage.cpp @@ -467,7 +467,7 @@ return subscribe(QStringLiteral("roomFiles"), QJsonDocument(params), id); } -RocketChatMessage::RocketChatMessageResult RocketChatMessage::login(const QString &username, const QString &password, quint64 id) +RocketChatMessage::RocketChatMessageResult RocketChatMessage::login(const QString &username, const QString &password, const QString &twoFactorAuthenticationCode, quint64 id) { QJsonObject user; user[QStringLiteral("username")] = username; @@ -480,8 +480,20 @@ passwordObject[QStringLiteral("algorithm")] = QStringLiteral("sha-256"); QJsonObject params; - params[QStringLiteral("password")] = passwordObject; - params[QStringLiteral("user")] = user; + if (!twoFactorAuthenticationCode.isEmpty()) { + // Note: This currently isn't documented. The message structure here follows the iOS client + // https://github.com/RocketChat/Rocket.Chat.iOS/blob/ba49216daa50097745f15855238ef8f4d6519bcf/Rocket.Chat/Managers/Model/AuthManager/AuthManagerSocket.swift#L152 + QJsonObject loginObject; + loginObject[QStringLiteral("user")] = user; + loginObject[QStringLiteral("password")] = passwordObject; + QJsonObject totpObject; + totpObject[QStringLiteral("code")] = twoFactorAuthenticationCode; + totpObject[QStringLiteral("login")] = loginObject; + params[QStringLiteral("totp")] = totpObject; + } else { + params[QStringLiteral("password")] = passwordObject; + params[QStringLiteral("user")] = user; + } return generateMethod(QStringLiteral("login"), QJsonDocument(params), id); }