aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2020-03-18 09:51:27 +0100
committerKitsune Ral <Kitsune-Ral@users.sf.net>2020-03-18 09:51:27 +0100
commitab3d0263b770e30de673c63740a5c26bcbf33e58 (patch)
treeee379ad12c459d16de2e29aff8721a61e437bc7a
parentd02a510a586db1a89476cd283ea24a281e9bb6af (diff)
downloadlibquotient-ab3d0263b770e30de673c63740a5c26bcbf33e58.tar.gz
libquotient-ab3d0263b770e30de673c63740a5c26bcbf33e58.zip
SsoSession and Connection::prepareForSso()
The response in the web browser is quite barebone, just enough to give feedback that things are alright. Closes #386. Closes #388.
-rw-r--r--CMakeLists.txt1
-rw-r--r--lib/connection.cpp6
-rw-r--r--lib/connection.h4
-rw-r--r--lib/ssosession.cpp127
-rw-r--r--lib/ssosession.h44
-rw-r--r--libquotient.pri2
6 files changed, 184 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 26394c9d..9975af91 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -137,6 +137,7 @@ set(lib_SRCS
lib/networkaccessmanager.cpp
lib/connectiondata.cpp
lib/connection.cpp
+ lib/ssosession.cpp
lib/logging.cpp
lib/room.cpp
lib/user.cpp
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 4b4d371a..0e6b1c84 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -321,6 +321,12 @@ void Connection::connectToServer(const QString& userId, const QString& password,
});
}
+SsoSession* Connection::prepareForSso(const QString& initialDeviceName,
+ const QString& deviceId)
+{
+ return new SsoSession(this, initialDeviceName, deviceId);
+}
+
void Connection::loginWithToken(const QByteArray& loginToken,
const QString& initialDeviceName,
const QString& deviceId)
diff --git a/lib/connection.h b/lib/connection.h
index 8f2abd0f..9b222ca8 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -18,6 +18,7 @@
#pragma once
+#include "ssosession.h"
#include "joinstate.h"
#include "qt_connection_util.h"
@@ -467,6 +468,9 @@ public:
std::forward<JobArgTs>(jobArgs)...);
}
+ Q_INVOKABLE SsoSession* prepareForSso(const QString& initialDeviceName,
+ const QString& deviceId = {});
+
/** Generate a new transaction id. Transaction id's are unique within
* a single Connection object
*/
diff --git a/lib/ssosession.cpp b/lib/ssosession.cpp
new file mode 100644
index 00000000..0f8f96e1
--- /dev/null
+++ b/lib/ssosession.cpp
@@ -0,0 +1,127 @@
+#include "ssosession.h"
+
+#include "connection.h"
+#include "csapi/sso_login_redirect.h"
+
+#include <QtNetwork/QTcpServer>
+#include <QtNetwork/QTcpSocket>
+#include <QtCore/QCoreApplication>
+#include <QtCore/QStringBuilder>
+
+using namespace Quotient;
+
+struct SsoSession::Private {
+ Private(SsoSession* q, const QString& initialDeviceName = {},
+ const QString& deviceId = {}, Connection* connection = nullptr)
+ : initialDeviceName(initialDeviceName)
+ , deviceId(deviceId)
+ , connection(connection)
+ {
+ auto* server = new QTcpServer(q);
+ server->listen();
+ // The "/returnToApplication" part is just a hint for the end-user,
+ // the callback will work without it equally well.
+ callbackUrl = QStringLiteral("http://localhost:%1/returnToApplication")
+ .arg(server->serverPort());
+ ssoUrl = connection->getUrlForApi<RedirectToSSOJob>(callbackUrl);
+
+ QObject::connect(server, &QTcpServer::newConnection, q, [this, server] {
+ qCDebug(MAIN) << "SSO callback initiated";
+ socket = server->nextPendingConnection();
+ server->close();
+ QObject::connect(socket, &QTcpSocket::readyRead, socket, [this] {
+ requestData.append(socket->readAll());
+ if (!socket->atEnd() && !requestData.endsWith("\r\n\r\n")) {
+ qDebug(MAIN) << "Incomplete request, waiting for more data";
+ return;
+ }
+ processCallback();
+ });
+ QObject::connect(socket, &QTcpSocket::disconnected, socket,
+ [this] { socket->deleteLater(); });
+ });
+ }
+ void processCallback();
+ void sendHttpResponse(const QByteArray& code, const QByteArray& msg);
+ void onError(const QByteArray& code, const QString& errorMsg);
+
+ QString initialDeviceName;
+ QString deviceId;
+ Connection* connection;
+ QString callbackUrl {};
+ QUrl ssoUrl {};
+ QTcpSocket* socket = nullptr;
+ QByteArray requestData {};
+};
+
+SsoSession::SsoSession(Connection* connection, const QString& initialDeviceName,
+ const QString& deviceId)
+ : QObject(connection)
+ , d(std::make_unique<Private>(this, initialDeviceName, deviceId, connection))
+{
+ qCDebug(MAIN) << "SSO session constructed";
+}
+
+SsoSession::~SsoSession()
+{
+ qCDebug(MAIN) << "SSO session deconstructed";
+}
+
+QUrl SsoSession::ssoUrl() const { return d->ssoUrl; }
+
+QUrl SsoSession::callbackUrl() const { return d->callbackUrl; }
+
+void SsoSession::Private::processCallback()
+{
+ // https://matrix.org/docs/guides/sso-for-client-developers
+ // Inspired by Clementine's src/internet/core/localredirectserver.cpp
+ // (see at https://github.com/clementine-player/Clementine/)
+ const auto& requestParts = requestData.split(' ');
+ if (requestParts.size() < 2 || requestParts[1].isEmpty()) {
+ onError("400 Bad Request", tr("No login token in SSO callback"));
+ return;
+ }
+ const auto& QueryItemName = QStringLiteral("loginToken");
+ QUrlQuery query { QUrl(requestParts[1]).query() };
+ if (!query.hasQueryItem(QueryItemName)) {
+ onError("400 Bad Request", tr("Malformed single sign-on callback"));
+ }
+ qCDebug(MAIN) << "Found the token in SSO callback, logging in";
+ connection->loginWithToken(query.queryItemValue(QueryItemName).toLatin1(),
+ initialDeviceName, deviceId);
+ connect(connection, &Connection::connected, socket, [this] {
+ const QString msg =
+ "The application '" % QCoreApplication::applicationName()
+ % "' has successfully logged in as a user " % connection->userId()
+ % " with device id " % connection->deviceId()
+ % ". This window can be closed. Thank you.\r\n";
+ sendHttpResponse("200 OK", msg.toHtmlEscaped().toUtf8());
+ socket->disconnectFromHost();
+ });
+ connect(connection, &Connection::loginError, socket, [this] {
+ onError("401 Unauthorised", tr("Login failed"));
+ socket->disconnectFromHost();
+ });
+}
+
+void SsoSession::Private::sendHttpResponse(const QByteArray& code,
+ const QByteArray& msg)
+{
+ socket->write("HTTP/1.0 ");
+ socket->write(code);
+ socket->write("\r\n"
+ "Content-type: text/html;charset=UTF-8\r\n"
+ "\r\n\r\n");
+ socket->write(msg);
+ socket->write("\r\n");
+}
+
+void SsoSession::Private::onError(const QByteArray& code,
+ const QString& errorMsg)
+{
+ qCWarning(MAIN).nospace() << errorMsg;
+ sendHttpResponse(code, "<h3>" + errorMsg.toUtf8() + "</h3>");
+ // [kitsune] Yeah, I know, dirty. Maybe the "right" way would be to have
+ // an intermediate signal but that seems just a fight for purity.
+ emit connection->loginError(errorMsg, requestData);
+}
diff --git a/lib/ssosession.h b/lib/ssosession.h
new file mode 100644
index 00000000..5845cd4d
--- /dev/null
+++ b/lib/ssosession.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <QtCore/QUrl>
+#include <QtCore/QObject>
+
+#include <memory>
+
+class QTcpServer;
+class QTcpSocket;
+
+namespace Quotient {
+class Connection;
+
+/*! Single sign-on (SSO) session encapsulation
+ *
+ * This class is responsible for setting up of a new SSO session, providing
+ * a URL to be opened (usually, in a web browser) and handling the callback
+ * response after completing the single sign-on, all the way to actually
+ * logging the user in. It does NOT open and render the SSO URL, it only does
+ * the necessary backstage work.
+ *
+ * Clients only need to open the URL; the rest is done for them.
+ * Client code can look something like:
+ * \code
+ * QDesktopServices::openUrl(
+ * connection->prepareForSso(initialDeviceName)->ssoUrl());
+ * \endcode
+ */
+class SsoSession : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(QUrl ssoUrl READ ssoUrl CONSTANT)
+ Q_PROPERTY(QUrl callbackUrl READ callbackUrl CONSTANT)
+public:
+ SsoSession(Connection* connection, const QString& initialDeviceName,
+ const QString& deviceId = {});
+ ~SsoSession() override;
+ QUrl ssoUrl() const;
+ QUrl callbackUrl() const;
+
+private:
+ class Private;
+ std::unique_ptr<Private> d;
+};
+} // namespace Quotient
diff --git a/libquotient.pri b/libquotient.pri
index 95d8694b..85b663ee 100644
--- a/libquotient.pri
+++ b/libquotient.pri
@@ -23,6 +23,7 @@ INCLUDEPATH += $$SRCPATH
HEADERS += \
$$SRCPATH/connectiondata.h \
$$SRCPATH/connection.h \
+ $$SRCPATH/ssosession.h \
$$SRCPATH/encryptionmanager.h \
$$SRCPATH/eventitem.h \
$$SRCPATH/room.h \
@@ -76,6 +77,7 @@ HEADERS += \
SOURCES += \
$$SRCPATH/connectiondata.cpp \
$$SRCPATH/connection.cpp \
+ $$SRCPATH/ssosession.cpp \
$$SRCPATH/encryptionmanager.cpp \
$$SRCPATH/eventitem.cpp \
$$SRCPATH/room.cpp \