Allow explicit selection of SSO method

fixes #975
This commit is contained in:
Nicolas Werner 2022-03-06 19:51:17 +01:00
parent ab05e2d8dc
commit 9482ac4e7a
No known key found for this signature in database
GPG Key ID: C8D75E610773F2D9
6 changed files with 129 additions and 53 deletions

View File

@ -405,7 +405,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG 25857f17272809ce2359f214d76fa11d46b1fa7e GIT_TAG e1b75074b501d2d3e0100d1170b3edef8a00799c
) )
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@ -692,7 +692,7 @@ if(USE_BUNDLED_COEURL)
FetchContent_Declare( FetchContent_Declare(
coeurl coeurl
GIT_REPOSITORY https://nheko.im/Nheko-Reborn/coeurl.git GIT_REPOSITORY https://nheko.im/Nheko-Reborn/coeurl.git
GIT_TAG v0.1.1 GIT_TAG v0.1.2
) )
FetchContent_MakeAvailable(coeurl) FetchContent_MakeAvailable(coeurl)
target_link_libraries(nheko PUBLIC coeurl::coeurl) target_link_libraries(nheko PUBLIC coeurl::coeurl)

View File

@ -177,8 +177,8 @@ modules:
- -Ddefault_library=static - -Ddefault_library=static
name: coeurl name: coeurl
sources: sources:
- commit: fa108b25a92b0e037723debc4388a300e737dc2d - commit: 1c530c153687c9072619f00ad77fff9960bdb048
tag: v0.1.1 tag: v0.1.2
type: git type: git
url: https://nheko.im/nheko-reborn/coeurl.git url: https://nheko.im/nheko-reborn/coeurl.git
- config-opts: - config-opts:
@ -189,7 +189,7 @@ modules:
buildsystem: cmake-ninja buildsystem: cmake-ninja
name: mtxclient name: mtxclient
sources: sources:
- commit: 25857f17272809ce2359f214d76fa11d46b1fa7e - commit: e1b75074b501d2d3e0100d1170b3edef8a00799c
#tag: v0.6.2 #tag: v0.6.2
type: git type: git
url: https://github.com/Nheko-Reborn/mtxclient.git url: https://github.com/Nheko-Reborn/mtxclient.git

View File

@ -6,6 +6,7 @@
import QtGraphicalEffects 1.12 import QtGraphicalEffects 1.12
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.5 import QtQuick.Controls 2.5
import QtQuick.Layouts 1.2
import im.nheko 1.0 import im.nheko 1.0
// FIXME(Nico): Don't use hardcoded colors. // FIXME(Nico): Don't use hardcoded colors.
@ -16,6 +17,8 @@ Button {
implicitWidth: Math.ceil(control.contentItem.implicitWidth + control.contentItem.implicitHeight) implicitWidth: Math.ceil(control.contentItem.implicitWidth + control.contentItem.implicitHeight)
hoverEnabled: true hoverEnabled: true
property string iconImage: ""
DropShadow { DropShadow {
anchors.fill: control.background anchors.fill: control.background
horizontalOffset: 3 horizontalOffset: 3
@ -27,16 +30,30 @@ Button {
source: control.background source: control.background
} }
contentItem: Text { contentItem: RowLayout {
text: control.text spacing: 0
//font: control.font anchors.centerIn: parent
font.capitalization: Font.AllUppercase Image {
font.pointSize: Math.ceil(fontMetrics.font.pointSize * 1.5) Layout.leftMargin: Nheko.paddingMedium
//font.capitalization: Font.AllUppercase Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
color: Nheko.colors.light Layout.preferredHeight: fontMetrics.font.pixelSize * 1.5
horizontalAlignment: Text.AlignHCenter Layout.preferredWidth: fontMetrics.font.pixelSize * 1.5
verticalAlignment: Text.AlignVCenter visible: !!iconImage
elide: Text.ElideRight source: iconImage
}
Text {
Layout.alignment: Qt.AlignHCenter
text: control.text
//font: control.font
font.capitalization: Font.AllUppercase
font.pointSize: Math.ceil(fontMetrics.font.pointSize * 1.5)
//font.capitalization: Font.AllUppercase
color: Nheko.colors.light
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
} }
background: Rectangle { background: Rectangle {

View File

@ -61,7 +61,7 @@ Item {
onEditingFinished: login.mxid = text onEditingFinished: login.mxid = text
ToolTip.text: qsTr("Your login name. A mxid should start with @ followed by the user id. After the user id you need to include your server name after a :.\nYou can also put your homeserver address there, if your server doesn't support .well-known lookup.\nExample: @user:server.my\nIf Nheko fails to discover your homeserver, it will show you a field to enter the server manually.") ToolTip.text: qsTr("Your login name. A mxid should start with @ followed by the user id. After the user id you need to include your server name after a :.\nYou can also put your homeserver address there, if your server doesn't support .well-known lookup.\nExample: @user:server.my\nIf Nheko fails to discover your homeserver, it will show you a field to enter the server manually.")
Keys.forwardTo: [pwBtn, ssoBtn] Keys.forwardTo: [pwBtn, ssoRepeater]
} }
@ -89,7 +89,7 @@ Item {
echoMode: TextInput.Password echoMode: TextInput.Password
ToolTip.text: qsTr("Your password.") ToolTip.text: qsTr("Your password.")
visible: login.passwordSupported visible: login.passwordSupported
Keys.forwardTo: [pwBtn, ssoBtn] Keys.forwardTo: [pwBtn, ssoRepeater]
} }
MatrixTextField { MatrixTextField {
@ -98,7 +98,7 @@ Item {
label: qsTr("Device name") label: qsTr("Device name")
placeholderText: login.initialDeviceName() placeholderText: login.initialDeviceName()
ToolTip.text: qsTr("A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.") ToolTip.text: qsTr("A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.")
Keys.forwardTo: [pwBtn, ssoBtn] Keys.forwardTo: [pwBtn, ssoRepeater]
} }
MatrixTextField { MatrixTextField {
@ -112,7 +112,7 @@ Item {
text: login.homeserver text: login.homeserver
onEditingFinished: login.homeserver = text onEditingFinished: login.homeserver = text
ToolTip.text: qsTr("The address that can be used to contact you homeservers client API.\nExample: https://server.my:8787") ToolTip.text: qsTr("The address that can be used to contact you homeservers client API.\nExample: https://server.my:8787")
Keys.forwardTo: [pwBtn, ssoBtn] Keys.forwardTo: [pwBtn, ssoRepeater]
} }
Item { Item {
@ -150,21 +150,28 @@ Item {
Keys.onReturnPressed: pwBtn.pwLogin() Keys.onReturnPressed: pwBtn.pwLogin()
Keys.enabled: pwBtn.enabled && login.passwordSupported Keys.enabled: pwBtn.enabled && login.passwordSupported
} }
FlatButton {
id: ssoBtn
visible: login.ssoSupported
enabled: login.homeserverValid && matrixIdLabel.text == login.mxid && login.homeserver == hsLabel.text
Layout.alignment: Qt.AlignHCenter
text: qsTr("SSO LOGIN")
function ssoLogin() {
login.onLoginButtonClicked(Login.SSO, matrixIdLabel.text, passwordLabel.text, deviceNameLabel.text)
}
onClicked: ssoBtn.ssoLogin()
Keys.onEnterPressed: ssoBtn.ssoLogin()
Keys.onReturnPressed: ssoBtn.ssoLogin()
Keys.enabled: ssoBtn.enabled && !login.passwordSupported
}
Repeater {
id: ssoRepeater
model: login.identityProviders
delegate: FlatButton {
id: ssoBtn
visible: login.ssoSupported
enabled: login.homeserverValid && matrixIdLabel.text == login.mxid && login.homeserver == hsLabel.text
Layout.alignment: Qt.AlignHCenter
text: modelData.name
iconImage: modelData.avatarUrl.replace("mxc://", "image://MxcImage/")
function ssoLogin() {
login.onLoginButtonClicked(Login.SSO, matrixIdLabel.text, modelData.id, deviceNameLabel.text)
}
onClicked: ssoBtn.ssoLogin()
Keys.onEnterPressed: ssoBtn.ssoLogin()
Keys.onReturnPressed: ssoBtn.ssoLogin()
Keys.enabled: ssoBtn.enabled && !login.passwordSupported
}
}
} }
} }

View File

@ -19,6 +19,7 @@
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
Q_DECLARE_METATYPE(LoginPage::LoginMethod) Q_DECLARE_METATYPE(LoginPage::LoginMethod)
Q_DECLARE_METATYPE(SSOProvider)
using namespace mtx::identifiers; using namespace mtx::identifiers;
@ -28,6 +29,7 @@ LoginPage::LoginPage(QObject *parent)
{ {
[[maybe_unused]] static auto ignored = [[maybe_unused]] static auto ignored =
qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod"); qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod");
[[maybe_unused]] static auto ignored2 = qRegisterMetaType<SSOProvider>();
connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection); connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection);
connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection); connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection);
@ -166,22 +168,47 @@ LoginPage::checkHomeserverVersion()
return; return;
} }
http::client()->get_login( http::client()->get_login([this](mtx::responses::LoginFlows flows,
[this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) { mtx::http::RequestErr err) {
if (err || flows.flows.empty()) if (err || flows.flows.empty())
emit versionOkCb(true, false); emit versionOkCb(true, false, {});
bool ssoSupported = false; QVariantList idps;
bool passwordSupported = false; bool ssoSupported = false;
for (const auto &flow : flows.flows) { bool passwordSupported = false;
if (flow.type == mtx::user_interactive::auth_types::sso) { for (const auto &flow : flows.flows) {
ssoSupported = true; if (flow.type == mtx::user_interactive::auth_types::sso) {
} else if (flow.type == mtx::user_interactive::auth_types::password) { ssoSupported = true;
passwordSupported = true;
} for (const auto &idp : flow.identity_providers) {
} SSOProvider prov;
emit versionOkCb(passwordSupported, ssoSupported); if (idp.brand == "apple")
}); prov.name_ = tr("Sign in with Apple");
else if (idp.brand == "facebook")
prov.name_ = tr("Continue with Facebook");
else if (idp.brand == "google")
prov.name_ = tr("Sign in with Google");
else if (idp.brand == "twitter")
prov.name_ = tr("Sign in with Twitter");
else
prov.name_ = tr("Login using %1").arg(QString::fromStdString(idp.name));
prov.avatarUrl_ = QString::fromStdString(idp.icon);
prov.id_ = QString::fromStdString(idp.id);
idps.push_back(QVariant::fromValue(prov));
}
if (flow.identity_providers.empty()) {
SSOProvider prov;
prov.name_ = tr("SSO LOGIN");
idps.push_back(QVariant::fromValue(prov));
}
} else if (flow.type == mtx::user_interactive::auth_types::password) {
passwordSupported = true;
}
}
emit versionOkCb(passwordSupported, ssoSupported, idps);
});
}); });
} }
@ -198,10 +225,11 @@ LoginPage::versionError(const QString &error)
} }
void void
LoginPage::versionOk(bool passwordSupported, bool ssoSupported) LoginPage::versionOk(bool passwordSupported, bool ssoSupported, QVariantList idps)
{ {
passwordSupported_ = passwordSupported; passwordSupported_ = passwordSupported;
ssoSupported_ = ssoSupported; ssoSupported_ = ssoSupported;
identityProviders_ = idps;
lookingUpHs_ = false; lookingUpHs_ = false;
homeserverValid_ = true; homeserverValid_ = true;
@ -287,8 +315,9 @@ LoginPage::onLoginButtonClicked(LoginMethod loginMethod,
sso->deleteLater(); sso->deleteLater();
}); });
QDesktopServices::openUrl( // password doubles as the idp id for SSO login
QString::fromStdString(http::client()->login_sso_redirect(sso->url()))); QDesktopServices::openUrl(QString::fromStdString(
http::client()->login_sso_redirect(sso->url(), password.toStdString())));
} }
loggingIn_ = true; loggingIn_ = true;

View File

@ -7,6 +7,7 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QVariantList>
namespace mtx { namespace mtx {
namespace responses { namespace responses {
@ -14,6 +15,23 @@ struct Login;
} }
} }
struct SSOProvider
{
Q_GADGET
Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString id READ id CONSTANT)
public:
[[nodiscard]] QString avatarUrl() const { return avatarUrl_; }
[[nodiscard]] QString name() const { return name_.toHtmlEscaped(); }
[[nodiscard]] QString id() const { return id_; }
QString avatarUrl_;
QString name_;
QString id_;
};
class LoginPage : public QObject class LoginPage : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -30,6 +48,8 @@ class LoginPage : public QObject
Q_PROPERTY(bool ssoSupported READ ssoSupported NOTIFY versionLookedUp) Q_PROPERTY(bool ssoSupported READ ssoSupported NOTIFY versionLookedUp)
Q_PROPERTY(bool homeserverNeeded READ homeserverNeeded NOTIFY versionLookedUp) Q_PROPERTY(bool homeserverNeeded READ homeserverNeeded NOTIFY versionLookedUp)
Q_PROPERTY(QVariantList identityProviders READ identityProviders NOTIFY versionLookedUp)
public: public:
enum class LoginMethod enum class LoginMethod
{ {
@ -51,6 +71,7 @@ public:
bool ssoSupported() const { return ssoSupported_; } bool ssoSupported() const { return ssoSupported_; }
bool homeserverNeeded() const { return homeserverNeeded_; } bool homeserverNeeded() const { return homeserverNeeded_; }
bool homeserverValid() const { return homeserverValid_; } bool homeserverValid() const { return homeserverValid_; }
QVariantList identityProviders() const { return identityProviders_; }
QString homeserver() { return homeserver_; } QString homeserver() { return homeserver_; }
QString mxid() { return mxid_; } QString mxid() { return mxid_; }
@ -89,7 +110,7 @@ signals:
//! Used to trigger the corresponding slot outside of the main thread. //! Used to trigger the corresponding slot outside of the main thread.
void versionErrorCb(const QString &err); void versionErrorCb(const QString &err);
void versionOkCb(bool passwordSupported, bool ssoSupported); void versionOkCb(bool passwordSupported, bool ssoSupported, QVariantList identityProviders);
void loginOk(const mtx::responses::Login &res); void loginOk(const mtx::responses::Login &res);
@ -116,7 +137,7 @@ public slots:
// Callback for errors produced during server probing // Callback for errors produced during server probing
void versionError(const QString &error_message); void versionError(const QString &error_message);
// Callback for successful server probing // Callback for successful server probing
void versionOk(bool passwordSupported, bool ssoSupported); void versionOk(bool passwordSupported, bool ssoSupported, QVariantList identityProviders);
private: private:
void checkHomeserverVersion(); void checkHomeserverVersion();
@ -137,6 +158,8 @@ private:
QString mxidError_; QString mxidError_;
QString error_; QString error_;
QVariantList identityProviders_;
bool passwordSupported_ = true; bool passwordSupported_ = true;
bool ssoSupported_ = false; bool ssoSupported_ = false;