From 5eb5c6628b9b8de5f11df6e0e4d04a450400c0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 29 Jan 2026 12:24:28 +0100 Subject: [PATCH] JsonRpc Server: Improve token verification handling depending on the interface configuration --- libnymea-core/jsonrpc/integrationshandler.cpp | 18 +-- .../jsonrpc/jsonrpcserverimplementation.cpp | 29 +++- libnymea-core/jsonrpc/usershandler.cpp | 147 ++++++++++++------ libnymea-core/usermanager/usermanager.cpp | 2 +- libnymea/jsonrpc/jsoncontext.cpp | 15 +- libnymea/jsonrpc/jsoncontext.h | 6 +- 6 files changed, 149 insertions(+), 68 deletions(-) diff --git a/libnymea-core/jsonrpc/integrationshandler.cpp b/libnymea-core/jsonrpc/integrationshandler.cpp index a26c24c2..2cee176c 100644 --- a/libnymea-core/jsonrpc/integrationshandler.cpp +++ b/libnymea-core/jsonrpc/integrationshandler.cpp @@ -789,7 +789,7 @@ JsonReply *IntegrationsHandler::GetThings(const QVariantMap ¶ms, const JsonC QVariantMap returns; QVariantList things; - if (NymeaCore::instance()->userManager()->hasRestrictedThingAccess(context.token())) { + if (NymeaCore::instance()->userManager()->hasRestrictedThingAccess(context.token()) && context.authenticationEnabled()) { // Restricted things access QList allowedThingIds = NymeaCore::instance()->userManager()->getAllowedThingIdsForToken(context.token()); if (params.contains("thingId")) { @@ -983,7 +983,7 @@ JsonReply *IntegrationsHandler::GetStateTypes(const QVariantMap ¶ms, const J JsonReply *IntegrationsHandler::GetStateValue(const QVariantMap ¶ms, const JsonContext &context) const { ThingId thingId(params.value("thingId").toString()); - if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) + if (context.authenticationEnabled() && !NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) return createReply(statusToReply(Thing::ThingErrorThingNotFound)); Thing *thing = m_thingManager->findConfiguredThing(thingId); @@ -1002,7 +1002,7 @@ JsonReply *IntegrationsHandler::GetStateValue(const QVariantMap ¶ms, const J JsonReply *IntegrationsHandler::GetStateValues(const QVariantMap ¶ms, const JsonContext &context) const { ThingId thingId(params.value("thingId").toString()); - if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) + if (context.authenticationEnabled() && !NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) return createReply(statusToReply(Thing::ThingErrorThingNotFound)); Thing *thing = m_thingManager->findConfiguredThing(thingId); @@ -1017,7 +1017,7 @@ JsonReply *IntegrationsHandler::GetStateValues(const QVariantMap ¶ms, const JsonReply *IntegrationsHandler::BrowseThing(const QVariantMap ¶ms, const JsonContext &context) const { ThingId thingId(params.value("thingId").toString()); - if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) + if (context.authenticationEnabled() && !NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) return createReply(statusToReply(Thing::ThingErrorThingNotFound)); QString itemId = params.value("itemId").toString(); @@ -1047,7 +1047,7 @@ JsonReply *IntegrationsHandler::BrowseThing(const QVariantMap ¶ms, const Jso JsonReply *IntegrationsHandler::GetBrowserItem(const QVariantMap ¶ms, const JsonContext &context) const { ThingId thingId(params.value("thingId").toString()); - if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) + if (context.authenticationEnabled() && !NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) return createReply(statusToReply(Thing::ThingErrorThingNotFound)); QString itemId = params.value("itemId").toString(); @@ -1072,7 +1072,7 @@ JsonReply *IntegrationsHandler::GetBrowserItem(const QVariantMap ¶ms, const JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap ¶ms, const JsonContext &context) { ThingId thingId(params.value("thingId").toString()); - if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) + if (context.authenticationEnabled() && !NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) return createReply(statusToReply(Thing::ThingErrorThingNotFound)); ActionTypeId actionTypeId(params.value("actionTypeId").toString()); @@ -1101,7 +1101,7 @@ JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap ¶ms, const J JsonReply *IntegrationsHandler::ExecuteBrowserItem(const QVariantMap ¶ms, const JsonContext &context) { ThingId thingId = ThingId(params.value("thingId").toString()); - if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) + if (context.authenticationEnabled() && !NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) return createReply(statusToReply(Thing::ThingErrorThingNotFound)); QString itemId = params.value("itemId").toString(); @@ -1126,7 +1126,7 @@ JsonReply *IntegrationsHandler::ExecuteBrowserItem(const QVariantMap ¶ms, co JsonReply *IntegrationsHandler::ExecuteBrowserItemAction(const QVariantMap ¶ms, const JsonContext &context) { ThingId thingId = ThingId(params.value("thingId").toString()); - if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) + if (context.authenticationEnabled() && !NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) return createReply(statusToReply(Thing::ThingErrorThingNotFound)); QString itemId = params.value("itemId").toString(); @@ -1153,7 +1153,7 @@ JsonReply *IntegrationsHandler::ExecuteBrowserItemAction(const QVariantMap ¶ JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap ¶ms, const JsonContext &context) { ThingId thingId = params.value("thingId").toUuid(); - if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) + if (context.authenticationEnabled() && !NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token())) return createReply(statusToReply(Thing::ThingErrorThingNotFound)); IOConnections ioConnections = m_thingManager->ioConnections(thingId); diff --git a/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp b/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp index e938593e..777e9b92 100644 --- a/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp +++ b/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp @@ -603,7 +603,7 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac } // check if authentication is required for this transport - if (interface->configuration().authenticationEnabled) { + if (interface->configuration().authenticationEnabled) { QStringList authExemptMethodsNoUser = {"JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser"}; QStringList authExemptMethodsWithUser = {"JSONRPC.Hello", "JSONRPC.Authenticate", "JSONRPC.RequestPushButtonAuth"}; // if there is no user in the system yet, let's fail unless this is a special method for authentication itself @@ -617,7 +617,7 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac return; } } else { - // ok, we have a user. if there isn't a valid token, let's fail unless this is a Authenticate, Introspect Hello call + // Ok, we have a user. If there isn't a valid token, let's fail unless this is an authentication related call if (!authExemptMethodsWithUser.contains(methodString)) { if (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token)) { sendUnauthorizedResponse(interface, clientId, commandId, "Forbidden: Invalid token."); @@ -681,7 +681,7 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac handler->setProperty("transportInterface", reinterpret_cast(interface)); } - JsonContext callContext(clientId, m_clientLocales.value(clientId)); + JsonContext callContext(clientId, m_clientLocales.value(clientId), interface->configuration().authenticationEnabled); callContext.setToken(token); qCDebug(dcJsonRpc()) << "Invoking method" << targetNamespace + '.' + method << "from client" << clientId; @@ -809,7 +809,9 @@ void JsonRPCServerImplementation::sendClientNotification(const QVariantMap ¶ continue; // Make sure this client is allowed to receive this notification - if (m_clientTokens.contains(clientId)) { + TransportInterface *transport = m_clientTransports.value(clientId, nullptr); + const bool authEnabled = transport ? transport->configuration().authenticationEnabled : true; + if (authEnabled && m_clientTokens.contains(clientId)) { const QByteArray token = m_clientTokens.value(clientId); if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, token)) { qCDebug(dcJsonRpc()) << "Not sending notification to client" << "to client" << clientId.toString() @@ -849,9 +851,24 @@ void JsonRPCServerImplementation::sendClientNotification(const QVariantMap ¶ { // Send client specific notifications qCDebug(dcJsonRpc()) << "Sending notification to client" << userInfo.username() << "connections..."; - foreach (const QByteArray &token, m_clientTokens) { + for (auto it = m_clientTokens.constBegin(); it != m_clientTokens.constEnd(); ++it) { + const QUuid clientId = it.key(); + const QByteArray token = it.value(); + + TransportInterface *transport = m_clientTransports.value(clientId, nullptr); + const bool authEnabled = transport ? transport->configuration().authenticationEnabled : true; + + if (!authEnabled) { + sendClientNotification(clientId, params); + continue; + } + + if (token.isEmpty()) { + continue; + } + if (NymeaCore::instance()->userManager()->tokenInfo(token).username() == userInfo.username()) { - sendClientNotification(m_clientTokens.key(token), params); + sendClientNotification(clientId, params); } } } diff --git a/libnymea-core/jsonrpc/usershandler.cpp b/libnymea-core/jsonrpc/usershandler.cpp index 8bbb4dff..2343cc5e 100644 --- a/libnymea-core/jsonrpc/usershandler.cpp +++ b/libnymea-core/jsonrpc/usershandler.cpp @@ -189,15 +189,21 @@ JsonReply *UsersHandler::ChangePassword(const QVariantMap ¶ms, const JsonCon QVariantMap returns; QByteArray currentToken = context.token(); - if (currentToken.isEmpty()) { - qCWarning(dcJsonRpc()) << "Cannot change password from an unauthenticated connection"; - returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); - return createReply(returns); - } + if (context.authenticationEnabled()) { + if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot change password from an unauthenticated connection"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } - if (!m_userManager->verifyToken(currentToken)) { - // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token - qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; + if (!m_userManager->verifyToken(currentToken)) { + // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token + qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } + } else if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot change password without token even if authentication is disabled for the transport"; returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); return createReply(returns); } @@ -216,15 +222,21 @@ JsonReply *UsersHandler::ChangeUserPassword(const QVariantMap ¶ms, const Jso QVariantMap returns; QByteArray currentToken = context.token(); - if (currentToken.isEmpty()) { - qCWarning(dcJsonRpc()) << "Cannot change a user password from an unauthenticated connection"; - returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); - return createReply(returns); - } + if (context.authenticationEnabled()) { + if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot change a user password from an unauthenticated connection"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } - if (!m_userManager->verifyToken(currentToken)) { - // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token - qCWarning(dcJsonRpc()) << "Invalid token. Cannot change a user password from an unauthenticated connection"; + if (!m_userManager->verifyToken(currentToken)) { + // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token + qCWarning(dcJsonRpc()) << "Invalid token. Cannot change a user password from an unauthenticated connection"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } + } else if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot change a user password without token even if authentication is disabled for the transport"; returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); return createReply(returns); } @@ -244,15 +256,21 @@ JsonReply *UsersHandler::GetUserInfo(const QVariantMap ¶ms, const JsonContex QVariantMap returns; QByteArray currentToken = context.token(); - if (currentToken.isEmpty()) { - qCWarning(dcJsonRpc()) << "Cannot get user info from an unauthenticated connection"; - returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); - return createReply(returns); - } + if (context.authenticationEnabled()) { + if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot get user info from an unauthenticated connection"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } - if (!m_userManager->verifyToken(currentToken)) { - // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token - qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; + if (!m_userManager->verifyToken(currentToken)) { + // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token + qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } + } else if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot get user info without token even if authentication is disabled for the transport"; returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); return createReply(returns); } @@ -272,15 +290,21 @@ JsonReply *UsersHandler::GetTokens(const QVariantMap ¶ms, const JsonContext QVariantMap returns; QByteArray currentToken = context.token(); - if (currentToken.isEmpty()) { - qCWarning(dcJsonRpc()) << "Cannot fetch tokens for an unauthenticated connection"; - returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); - return createReply(returns); - } + if (context.authenticationEnabled()) { + if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot fetch tokens for an unauthenticated connection"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } - if (!m_userManager->verifyToken(currentToken)) { - // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token - qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; + if (!m_userManager->verifyToken(currentToken)) { + // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token + qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } + } else if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot fetch tokens without token even if authentication is disabled for the transport"; returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); return createReply(returns); } @@ -302,15 +326,21 @@ JsonReply *UsersHandler::GetUserTokens(const QVariantMap ¶ms, const JsonCont QVariantMap returns; QByteArray currentToken = context.token(); - if (currentToken.isEmpty()) { - qCWarning(dcJsonRpc()) << "Cannot fetch tokens for an unauthenticated connection"; - returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); - return createReply(returns); - } + if (context.authenticationEnabled()) { + if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot fetch tokens for an unauthenticated connection"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } - if (!m_userManager->verifyToken(currentToken)) { - // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token - qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; + if (!m_userManager->verifyToken(currentToken)) { + // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token + qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } + } else if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot fetch tokens without token even if authentication is disabled for the transport"; returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); return createReply(returns); } @@ -333,15 +363,21 @@ JsonReply *UsersHandler::RemoveToken(const QVariantMap ¶ms, const JsonContex QVariantMap returns; QByteArray currentToken = context.token(); - if (currentToken.isEmpty()) { - qCWarning(dcJsonRpc()) << "Cannot remove a token from an unauthenticated connection."; - returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); - return createReply(returns); - } + if (context.authenticationEnabled()) { + if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot remove a token from an unauthenticated connection."; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } - if (!m_userManager->verifyToken(currentToken)) { - // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token - qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; + if (!m_userManager->verifyToken(currentToken)) { + // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token + qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } + } else if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot remove a token without token even if authentication is disabled for the transport."; returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); return createReply(returns); } @@ -414,7 +450,20 @@ JsonReply *UsersHandler::SetUserInfo(const QVariantMap ¶ms, const JsonContex { QVariantMap returns; - TokenInfo callingTokenInfo = m_userManager->tokenInfo(context.token()); + QByteArray currentToken = context.token(); + if (context.authenticationEnabled()) { + if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot set user info from an unauthenticated connection"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } + } else if (currentToken.isEmpty()) { + qCWarning(dcJsonRpc()) << "Cannot set user info without token even if authentication is disabled for the transport"; + returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(returns); + } + + TokenInfo callingTokenInfo = m_userManager->tokenInfo(currentToken); QString username; if (params.contains("username")) { diff --git a/libnymea-core/usermanager/usermanager.cpp b/libnymea-core/usermanager/usermanager.cpp index 88882f5a..821b7682 100644 --- a/libnymea-core/usermanager/usermanager.cpp +++ b/libnymea-core/usermanager/usermanager.cpp @@ -534,7 +534,7 @@ QList UserManager::tokens(const QString &username) const TokenInfo UserManager::tokenInfo(const QByteArray &token) const { if (!validateToken(token)) { - qCWarning(dcUserManager) << "Token did not pass validation:" << token; + qCWarning(dcUserManager()) << "Token did not pass validation:" << token; return TokenInfo(); } diff --git a/libnymea/jsonrpc/jsoncontext.cpp b/libnymea/jsonrpc/jsoncontext.cpp index 260fb4c2..78b3e6ec 100644 --- a/libnymea/jsonrpc/jsoncontext.cpp +++ b/libnymea/jsonrpc/jsoncontext.cpp @@ -24,9 +24,10 @@ #include "jsoncontext.h" -JsonContext::JsonContext(const QUuid &clientId, const QLocale &locale): +JsonContext::JsonContext(const QUuid &clientId, const QLocale &locale, bool authenticationEnabled): m_clientId(clientId), - m_locale(locale) + m_locale(locale), + m_authenticationEnabled(authenticationEnabled) { } @@ -50,3 +51,13 @@ void JsonContext::setToken(const QByteArray &token) { m_token = token; } + +bool JsonContext::authenticationEnabled() const +{ + return m_authenticationEnabled; +} + +void JsonContext::setAuthenticationEnabled(bool authenticationEnabled) +{ + m_authenticationEnabled = authenticationEnabled; +} diff --git a/libnymea/jsonrpc/jsoncontext.h b/libnymea/jsonrpc/jsoncontext.h index 182b79aa..32a54360 100644 --- a/libnymea/jsonrpc/jsoncontext.h +++ b/libnymea/jsonrpc/jsoncontext.h @@ -31,7 +31,7 @@ class JsonContext { public: - JsonContext(const QUuid &clientId, const QLocale &locale); + JsonContext(const QUuid &clientId, const QLocale &locale, bool authenticationEnabled = true); QUuid clientId() const; QLocale locale() const; @@ -39,10 +39,14 @@ public: QByteArray token() const; void setToken(const QByteArray &token); + bool authenticationEnabled() const; + void setAuthenticationEnabled(bool authenticationEnabled); + private: QUuid m_clientId; QLocale m_locale; QByteArray m_token; + bool m_authenticationEnabled = true; }; #endif // JSONCONTEXT_H