Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC-awareness #850

Merged
merged 7 commits into from
Jan 12, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions Quotient/connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ void Connection::loginWithPassword(const QString& userId,
const QString& initialDeviceName,
const QString& deviceId)
{
d->ensureHomeserver(userId, LoginFlows::Password).then([=, this] {
d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId),
d->ensureHomeserver(userId, LoginFlowTypes::Password).then([=, this] {
d->loginToServer(LoginFlowTypes::Password, makeUserIdentifier(userId),
password, /*token*/ QString(), deviceId, initialDeviceName);
});
}
Expand All @@ -172,8 +172,8 @@ void Connection::loginWithToken(const QString& loginToken,
const QString& initialDeviceName,
const QString& deviceId)
{
Q_ASSERT(d->data->baseUrl().isValid() && d->loginFlows.contains(LoginFlows::Token));
d->loginToServer(LoginFlows::Token.type, std::nullopt /*user is encoded in loginToken*/,
Q_ASSERT(d->data->baseUrl().isValid() && d->supportsLoginFlow(LoginFlowTypes::Token));
d->loginToServer(LoginFlowTypes::Token, std::nullopt /*user is encoded in loginToken*/,
QString() /*password*/, loginToken, deviceId, initialDeviceName);
}

Expand Down Expand Up @@ -355,28 +355,28 @@ void Connection::Private::completeSetup(const QString& mxId, bool newLogin,
}

QFuture<void> Connection::Private::ensureHomeserver(const QString& userId,
const std::optional<LoginFlow>& flow)
const LoginFlowType& flowType)
{
QPromise<void> promise;
auto result = promise.future();
promise.start();
if (data->baseUrl().isValid() && (!flow || loginFlows.contains(*flow))) {
if (data->baseUrl().isValid() && (flowType.isEmpty() || supportsLoginFlow(flowType))) {
q->setObjectName(userId % u"(?)");
promise.finish(); // Perfect, we're already good to go
} else if (userId.startsWith(u'@') && userId.indexOf(u':') != -1) {
// Try to ascertain the homeserver URL and flows
q->setObjectName(userId % u"(?)");
q->resolveServer(userId);
if (flow)
if (!flowType.isEmpty())
QtFuture::connect(q, &Connection::loginFlowsChanged)
.then([this, flow, p = std::move(promise)]() mutable {
if (loginFlows.contains(*flow))
.then([this, flowType, p = std::move(promise)]() mutable {
if (supportsLoginFlow(flowType))
p.finish();
else // Leave the promise unfinished and emit the error
emit q->loginError(tr("Unsupported login flow"),
tr("The homeserver at %1 does not support"
" the login flow '%2'")
.arg(data->baseUrl().toDisplayString(), flow->type));
" login flows of type '%2'")
.arg(data->baseUrl().toDisplayString(), flowType));
});
else // Any flow is fine, just wait until the homeserver is resolved
return QFuture<void>(QtFuture::connect(q, &Connection::homeserverChanged));
Expand Down Expand Up @@ -960,14 +960,25 @@ QVector<GetLoginFlowsJob::LoginFlow> Connection::loginFlows() const
return d->loginFlows;
}

std::optional<LoginFlow> Connection::getLoginFlow(const QString& flowType) const
{
if (auto it = std::ranges::find(d->loginFlows, flowType, &LoginFlow::type);
it != d->loginFlows.cend())
return *it;
return std::nullopt;
}

bool Connection::supportsPasswordAuth() const
{
return d->loginFlows.contains(LoginFlows::Password);
if (auto ssoFlow = getLoginFlow(LoginFlowTypes::SSO);
ssoFlow && ssoFlow->delegatedOidcCompatibility)
return false; // See MSC3824
return d->supportsLoginFlow(LoginFlowTypes::Password);
}

bool Connection::supportsSso() const
{
return d->loginFlows.contains(LoginFlows::SSO);
return d->supportsLoginFlow(LoginFlowTypes::SSO);
}

Room* Connection::room(const QString& roomId, JoinStates states) const
Expand Down
23 changes: 19 additions & 4 deletions Quotient/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,34 @@ class QOlmAccount;
class QOlmInboundGroupSession;

using LoginFlow = GetLoginFlowsJob::LoginFlow;
using LoginFlowType = QString;

//! Predefined login flow types
namespace LoginFlowTypes {
inline constexpr auto Password = "m.login.password"_L1, SSO = "m.login.sso"_L1,
Token = "m.login.token"_L1;
}

//! Predefined login flows
namespace LoginFlows {
inline const LoginFlow Password { "m.login.password"_L1 };
inline const LoginFlow SSO { "m.login.sso"_L1 };
inline const LoginFlow Token { "m.login.token"_L1 };
namespace LoginFlows
#ifndef Q_MOC_RUN
[[deprecated("Use login flow types and Connection::getLoginFlow() instead")]]
#endif
{
inline const LoginFlow Password { LoginFlowTypes::Password };
inline const LoginFlow SSO { LoginFlowTypes::SSO };
inline const LoginFlow Token { LoginFlowTypes::Token };
}

// To simplify comparisons of LoginFlows

[[deprecated("Compare login flow types instead")]]
inline bool operator==(const LoginFlow& lhs, const LoginFlow& rhs)
{
return lhs.type == rhs.type;
}

[[deprecated("Compare login flow types instead")]]
inline bool operator!=(const LoginFlow& lhs, const LoginFlow& rhs)
{
return !(lhs == rhs);
Expand Down Expand Up @@ -297,6 +310,8 @@ class QUOTIENT_API Connection : public QObject {
bool isUsable() const;
//! Get the list of supported login flows
QVector<GetLoginFlowsJob::LoginFlow> loginFlows() const;
//! Get the login flow of a given type
Q_INVOKABLE std::optional<LoginFlow> getLoginFlow(const QString& flowType) const;
//! Check whether the current homeserver supports password auth
bool supportsPasswordAuth() const;
//! Check whether the current homeserver supports SSO
Expand Down
15 changes: 11 additions & 4 deletions Quotient/connection_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "connection.h"
#include "connectiondata.h"
#include "connectionencryptiondata_p.h"
#include "ranges_extras.h"
#include "settings.h"
#include "syncdata.h"

Expand Down Expand Up @@ -76,21 +77,27 @@ class Q_DECL_HIDDEN Quotient::Connection::Private {
!= "json"_L1;
bool lazyLoading = false;

bool supportsLoginFlow(const LoginFlowType& flowType) const
{
return rangeContains(loginFlows, flowType, &LoginFlow::type);
}

//! \brief Check the homeserver and resolve it if needed, before connecting
//!
//! A single entry for functions that need to check whether the homeserver is valid before
//! running. Emits resolveError() if the homeserver URL is not valid and cannot be resolved
//! from \p userId; loginError() if the homeserver is accessible but doesn't support \p flow.
//! from \p userId; loginError() if the homeserver is accessible but doesn't support \p
//! flowType.
//!
//! \param userId fully-qualified MXID to resolve HS from
//! \param flow optionally, a login flow that should be supported;
//! `std::nullopt`, if there are no login flow requirements
//! \param flowType optionally, a login flowType that should be supported;
//! `std::nullopt`, if there are no login flowType requirements
//! \return a future that becomes ready once the homeserver is available; if the homeserver
//! URL is incorrect or other problems occur, the future is never resolved and is
//! deleted (along with associated continuations) as soon as the problem becomes
//! apparent
//! \sa resolveServer, resolveError, loginError
QFuture<void> ensureHomeserver(const QString& userId, const std::optional<LoginFlow>& flow = {});
QFuture<void> ensureHomeserver(const QString& userId, const LoginFlowType& flowType = {});
template <typename... LoginArgTs>
void loginToServer(LoginArgTs&&... loginArgs);
void completeSetup(const QString& mxId, bool newLogin = true,
Expand Down
4 changes: 4 additions & 0 deletions Quotient/csapi/login.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class QUOTIENT_API GetLoginFlowsJob : public BaseJob {
//! necessarily indicate that the user attempting to log in will
//! be able to generate such a token.
bool getLoginToken{ false };

bool delegatedOidcCompatibility{ false };
};

// Construction/destruction
Expand All @@ -55,6 +57,8 @@ struct QUOTIENT_API JsonObjectConverter<GetLoginFlowsJob::LoginFlow> {
{
fillFromJson(jo.value("type"_L1), result.type);
fillFromJson(jo.value("get_login_token"_L1), result.getLoginToken);
fillFromJson(jo.value("org.matrix.msc3824.delegated_oidc_compatibility"_L1),
result.delegatedOidcCompatibility);
}
};

Expand Down
9 changes: 9 additions & 0 deletions Quotient/ranges_extras.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,13 @@ template <template <typename> class TargetT, typename SourceT>
#endif
}

#ifdef __cpp_lib_ranges_contains
constexpr auto rangeContains = std::ranges::contains;
#else
[[nodiscard]] constexpr auto rangeContains(const auto& c, const auto& v, auto proj)
{
return std::ranges::find(c, v, std::move(proj)) != std::ranges::end(c);
}
#endif

}
14 changes: 13 additions & 1 deletion Quotient/ssosession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,19 @@ SsoSession::SsoSession(Connection* connection, const QString& initialDeviceName,
, d(makeImpl<Private>(this, initialDeviceName, deviceId, connection))
{}

QUrl SsoSession::ssoUrl() const { return d->ssoUrl; }
namespace {
QUrl withAction(QUrl url, const QString& value)
{
QUrlQuery q{ url.query() };
q.addQueryItem(u"action"_s, value);
url.setQuery(q);
return url;
}
}

QUrl SsoSession::ssoUrl() const { return withAction(d->ssoUrl, u"login"_s); }

QUrl SsoSession::ssoUrlForRegistration() const { return withAction(d->ssoUrl, u"register"_s); }

QUrl SsoSession::callbackUrl() const { return QUrl(d->callbackUrl); }

Expand Down
1 change: 1 addition & 0 deletions Quotient/ssosession.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class QUOTIENT_API SsoSession : public QObject {
~SsoSession() override = default;

QUrl ssoUrl() const;
QUrl ssoUrlForRegistration() const;
QUrl callbackUrl() const;

private:
Expand Down
1 change: 1 addition & 0 deletions gtad/gtad.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ analyzer:
origin_server_ts: originServerTimestamp # Instead of originServerTs
start: begin # Because start() is a method in BaseJob
/^.*/m\.([a-z].*)$/: '$1' # Strip leading m. from all identifiers
/^.*/org\.matrix\.msc\d{4}\.([a-z].*)$/: '$1' # Strip m.org's unstable prefixes from identifiers
m.3pid_changes: ThirdPartyIdChanges # Special case because there's a digit after m.
AuthenticationData/additionalProperties: authInfo
/^/(Location|Protocol|User)$/: 'ThirdParty$1'
Expand Down
Loading