aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2020-03-26 19:14:34 +0100
committerKitsune Ral <Kitsune-Ral@users.sf.net>2020-03-26 22:52:01 +0100
commit123d58fc3c29d8e9adbb5b654df53d5cbb0a32fa (patch)
treeb395f17139b0e7de576670e347138059600e4440 /lib
parent1b5d6216beddd17c820240dd4bdeaf9c47624cf6 (diff)
downloadlibquotient-123d58fc3c29d8e9adbb5b654df53d5cbb0a32fa.tar.gz
libquotient-123d58fc3c29d8e9adbb5b654df53d5cbb0a32fa.zip
SsoSession and Connection::prepareForSso()
Final part of #388 backport.
Diffstat (limited to 'lib')
-rw-r--r--lib/connection.cpp6
-rw-r--r--lib/connection.h4
-rw-r--r--lib/ssosession.cpp127
-rw-r--r--lib/ssosession.h44
4 files changed, 181 insertions, 0 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 6da04118..647fc006 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -254,6 +254,12 @@ void Connection::doConnectToServer(const QString& user, const QString& password,
password, /*token*/ "", deviceId, initialDeviceName);
}
+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 782ffd2c..33f9bfba 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -19,6 +19,7 @@
#pragma once
#include "csapi/login.h"
+#include "ssosession.h"
#include "csapi/create_room.h"
#include "joinstate.h"
#include "events/accountdataevents.h"
@@ -415,6 +416,9 @@ namespace QMatrixClient
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..6ea4a3f5
--- /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 QMatrixClient;
+
+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..af20c075
--- /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 QMatrixClient {
+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 QMatrixClient