From 294646a14167503994e7746e67da6fb1edffe9e1 Mon Sep 17 00:00:00 2001 From: Kasen IO Date: Mon, 27 Jul 2020 23:04:59 -0400 Subject: [PATCH 01/22] Add OAuth2 to domain settings. --- .../resources/describe-settings.json | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 3ae92651b8..f6b6656d7d 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -3,7 +3,7 @@ "settings": [ { "name": "metaverse", - "label": "Metaverse / Networking", + "label": "Networking / Metaverse", "settings": [ { "name": "access_token", @@ -57,6 +57,41 @@ } ] }, + { + "name": "authentication", + "label": "Networking / Authentication", + "settings": [ + { + "name": "enable_oauth2", + "label": "Enable OAuth2 Authentication", + "help": "Allow a WordPress-based OAuth2 service to assign users to groups based on their role with the service.", + "default": false, + "type": "checkbox", + "advanced": true + }, + { + "name": "require_oauth2", + "label": "Require OAuth2 Authentication", + "help": "For any users not explicitly authorized in these settings, notify the Interface to authenticate through this method.", + "default": false, + "type": "checkbox", + "advanced": true + }, + { + "name": "domain_access_token", + "label": "Domain API Access Token", + "help": "This is the access token that your domain-server will use to verify users and their roles. This token must grant access to that permission set on your REST API server.", + "advanced": true, + "backup": false + }, + { + "name": "authentication_oauth2_url_base", + "label": "Authentication URL Base", + "help": "The URL base that the Interface and domain-server will use to make API requests.", + "advanced": true + } + ] + }, { "label": "Monitoring", "name": "monitoring", From 5706a42b56de695c0f1135814fc24be60c69626f Mon Sep 17 00:00:00 2001 From: Kasen IO Date: Wed, 29 Jul 2020 01:53:21 -0400 Subject: [PATCH 02/22] Implement further handling of Interface OAuth2 This is preliminary, needs to be revisited in a more dynamic and clean fashion with time. --- interface/src/ui/LoginDialog.cpp | 12 +- .../networking/src/DomainAccountManager.cpp | 122 +++++++++++++++++- .../networking/src/DomainAccountManager.h | 11 +- 3 files changed, 134 insertions(+), 11 deletions(-) diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 94800529b9..ac1aa95f19 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -25,6 +25,7 @@ #include #include "AccountManager.h" +#include "DomainAccountManager.h" #include "DependencyManager.h" #include "DialogsManager.h" #include "Menu.h" @@ -40,12 +41,17 @@ const QUrl LOGIN_DIALOG = PathUtils::qmlUrl("OverlayLoginDialog.qml"); LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent) { auto accountManager = DependencyManager::get(); + auto domainAccountManager = DependencyManager::get(); // the login hasn't been dismissed yet if the user isn't logged in and is encouraged to login. #if !defined(Q_OS_ANDROID) connect(accountManager.data(), &AccountManager::loginComplete, this, &LoginDialog::handleLoginCompleted); connect(accountManager.data(), &AccountManager::loginFailed, this, &LoginDialog::handleLoginFailed); + connect(domainAccountManager.data(), &DomainAccountManager::loginComplete, + this, &LoginDialog::handleLoginCompleted); + connect(domainAccountManager.data(), &DomainAccountManager::loginFailed, + this, &LoginDialog::handleLoginFailed); connect(qApp, &Application::loginDialogFocusEnabled, this, &LoginDialog::focusEnabled); connect(qApp, &Application::loginDialogFocusDisabled, this, &LoginDialog::focusDisabled); connect(this, SIGNAL(dismissedLoginDialog()), qApp, SLOT(onDismissedLoginDialog())); @@ -139,11 +145,7 @@ void LoginDialog::login(const QString& username, const QString& password) const void LoginDialog::loginDomain(const QString& username, const QString& password, const QString& domainAuthProvider) const { qDebug() << "Attempting to login" << username << "into a domain through" << domainAuthProvider; - // ####### TODO - // DependencyManager::get()->requestAccessToken(username, password, domainAuthProvider); - - // ####### TODO: It may not be necessary to pass domainAuthProvider to the login dialog and through to here because it was - // originally provided to the QML from C++. + DependencyManager::get()->requestAccessToken(username, password, domainAuthProvider); } void LoginDialog::loginThroughOculus() { diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index d1da09817c..524a7c4b0a 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -11,18 +11,132 @@ #include "DomainAccountManager.h" -#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DomainAccountManager.h" +#include "NetworkingConstants.h" +#include "OAuthAccessToken.h" +#include "NetworkLogging.h" +#include "NodeList.h" +#include "udt/PacketHeaders.h" +#include "NetworkAccessManager.h" + +const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; +const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner"; + +Setting::Handle domainAccessToken {"private/domainAccessToken", "" }; +Setting::Handle domainAccessRefreshToken {"private/domainAccessToken", "" }; +Setting::Handle domainAccessTokenExpiresIn {"private/domainAccessTokenExpiresIn", -1 }; +Setting::Handle domainAccessTokenType {"private/domainAccessTokenType", "" }; + +QUrl _domainAuthProviderURL; + +// FIXME: If you try to authenticate this way on another domain, no one knows what will happen. Probably death. DomainAccountManager::DomainAccountManager() { - + connect(this, &DomainAccountManager::loginComplete, this, &DomainAccountManager::sendInterfaceAccessTokenToServer); } +void DomainAccountManager::requestAccessToken(const QString& login, const QString& password, const QString& domainAuthProvider) { + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest request; + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); + + _domainAuthProviderURL = domainAuthProvider; + _domainAuthProviderURL.setPath("/oauth/token"); + + QByteArray postData; + postData.append("grant_type=password&"); + postData.append("username=" + QUrl::toPercentEncoding(login) + "&"); + postData.append("password=" + QUrl::toPercentEncoding(password) + "&"); + postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); + + request.setUrl(_domainAuthProviderURL); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QNetworkReply* requestReply = networkAccessManager.post(request, postData); + connect(requestReply, &QNetworkReply::finished, this, &DomainAccountManager::requestAccessTokenFinished); +} + +void DomainAccountManager::requestAccessTokenFinished() { + QNetworkReply* requestReply = reinterpret_cast(sender()); + + QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); + const QJsonObject& rootObject = jsonResponse.object(); + + if (!rootObject.contains("error")) { + // construct an OAuthAccessToken from the json object + + if (!rootObject.contains("access_token") || !rootObject.contains("expires_in") + || !rootObject.contains("token_type")) { + // TODO: error handling - malformed token response + qCDebug(networking) << "Received a response for password grant that is missing one or more expected values."; + } else { + // clear the path from the response URL so we have the right root URL for this access token + QUrl rootURL = requestReply->url(); + rootURL.setPath(""); + + qCDebug(networking) << "Storing a domain account with access-token for" << qPrintable(rootURL.toString()); + + setAccessTokenFromJSON(rootObject); + + emit loginComplete(rootURL); + } + } else { + qCDebug(networking) << "Error in response for password grant -" << rootObject["error_description"].toString(); + emit loginFailed(); + } +} + +void DomainAccountManager::sendInterfaceAccessTokenToServer() { + // TODO: Send successful packet to the domain-server. +} + +bool DomainAccountManager::accessTokenIsExpired() { + return domainAccessTokenExpiresIn.get() != -1 && domainAccessTokenExpiresIn.get() <= QDateTime::currentMSecsSinceEpoch(); +} + + bool DomainAccountManager::hasValidAccessToken() { + QString currentDomainAccessToken = domainAccessToken.get(); + + if (currentDomainAccessToken.isEmpty() || accessTokenIsExpired()) { - // #######: TODO + if (VERBOSE_HTTP_REQUEST_DEBUGGING) { + qCDebug(networking) << "An access token is required for requests to" + << qPrintable(_domainAuthProviderURL.toString()); + } - return false; + return false; + } else { + + // ####### TODO:: + + // if (!_isWaitingForTokenRefresh && needsToRefreshToken()) { + // refreshAccessToken(); + // } + + return true; + } + +} + +void DomainAccountManager::setAccessTokenFromJSON(const QJsonObject& jsonObject) { + domainAccessToken.set(jsonObject["access_token"].toString()); + domainAccessRefreshToken.set(jsonObject["refresh_token"].toString()); + domainAccessTokenExpiresIn.set(QDateTime::currentMSecsSinceEpoch() + (jsonObject["expires_in"].toDouble() * 1000)); + domainAccessTokenType.set(jsonObject["token_type"].toString()); } bool DomainAccountManager::checkAndSignalForAccessToken() { diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index 696df71ab1..4bb197175e 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -25,15 +25,22 @@ public: Q_INVOKABLE bool checkAndSignalForAccessToken(); public slots: - + void requestAccessToken(const QString& login, const QString& password, const QString& domainAuthProvider); + + void requestAccessTokenFinished(); signals: void authRequired(); + void loginComplete(const QUrl& authURL); + void loginFailed(); + void logoutComplete(); private slots: private: bool hasValidAccessToken(); - + bool accessTokenIsExpired(); + void setAccessTokenFromJSON(const QJsonObject&); + void sendInterfaceAccessTokenToServer(); }; #endif // hifi_DomainAccountManager_h From af34536a09aea56ccb8fdadd9ffe859f98dec906 Mon Sep 17 00:00:00 2001 From: Kasen IO Date: Thu, 30 Jul 2020 00:34:02 -0400 Subject: [PATCH 03/22] Further complete the loop. --- domain-server/resources/describe-settings.json | 4 ++-- interface/src/ui/DialogsManager.cpp | 15 ++++++++++----- interface/src/ui/DialogsManager.h | 2 ++ interface/src/ui/LoginDialog.cpp | 4 +--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index f6b6656d7d..281a0be9cc 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -59,12 +59,12 @@ }, { "name": "authentication", - "label": "Networking / Authentication", + "label": "Networking / WordPress OAuth2", "settings": [ { "name": "enable_oauth2", "label": "Enable OAuth2 Authentication", - "help": "Allow a WordPress-based OAuth2 service to assign users to groups based on their role with the service.", + "help": "Allow a WordPress-based (miniOrange) OAuth2 service to assign users to groups based on their role with the service.", "default": false, "type": "checkbox", "advanced": true diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 848663967f..e96b8c5626 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -29,6 +29,7 @@ #include "OctreeStatsDialog.h" #include "PreferencesDialog.h" #include "UpdateDialog.h" +#include "DomainHandler.h" #include "scripting/HMDScriptingInterface.h" @@ -130,15 +131,19 @@ void DialogsManager::hideLoginDialog() { void DialogsManager::showDomainLoginDialog() { + const QJsonObject& settingsObject = DependencyManager::get()->getDomainHandler().getSettingsObject(); + static const QString WP_OAUTH2_SERVER_URL = "authentication_oauth2_url_base"; + + if (!settingsObject.contains(WP_OAUTH2_SERVER_URL)) { + qDebug() << "Cannot log in to domain because an OAuth2 authorization was required but no authorization server was given."; + return; + } + + _domainLoginAuthProvider = settingsObject[WP_OAUTH2_SERVER_URL].toString(); _isDomainLogin = true; LoginDialog::showWithSelection(); } -// #######: TODO: Domain version of toggleLoginDialog()? - -// #######: TODO: Domain version of hiadLoginDialog()? - - void DialogsManager::showUpdateDialog() { UpdateDialog::show(); } diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 30127ced68..b76ff69386 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -42,6 +42,7 @@ public: void emitAddressBarShown(bool visible) { emit addressBarShown(visible); } void setAddressBarVisible(bool addressBarVisible); bool getIsDomainLogin() { return _isDomainLogin; } + QString getDomainLoginAuthProvider() { return _domainLoginAuthProvider; } public slots: void showAddressBar(); @@ -87,6 +88,7 @@ private: bool _addressBarVisible { false }; bool _isDomainLogin { false }; + QString _domainLoginAuthProvider { "" }; }; #endif // hifi_DialogsManager_h diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index ac1aa95f19..d64ebdf42a 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -428,8 +428,6 @@ bool LoginDialog::getDomainLoginRequested() const { return DependencyManager::get()->getIsDomainLogin(); } -// ####### TODO: This method may not be necessary. QString LoginDialog::getDomainLoginAuthProvider() const { - // ####### TODO - return QString("https://example.com/oauth2"); + return DependencyManager::get()->getDomainLoginAuthProvider(); } From 856e1f1ca3826a2065f6ddae182f218f21fcbd84 Mon Sep 17 00:00:00 2001 From: kasenvr <52365539+kasenvr@users.noreply.github.com> Date: Thu, 30 Jul 2020 22:25:24 -0400 Subject: [PATCH 04/22] Update interface/src/ui/DialogsManager.cpp Co-authored-by: David Rowe --- interface/src/ui/DialogsManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index e96b8c5626..c61d4c4069 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -135,7 +135,7 @@ void DialogsManager::showDomainLoginDialog() { static const QString WP_OAUTH2_SERVER_URL = "authentication_oauth2_url_base"; if (!settingsObject.contains(WP_OAUTH2_SERVER_URL)) { - qDebug() << "Cannot log in to domain because an OAuth2 authorization was required but no authorization server was given."; + qDebug() << "Cannot log in to domain because an OAuth2 authorization was required but no authorization server was specified."; return; } From 6b28f3ea0dd1536bd480403bb8c3e3407cd7b99e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 31 Jul 2020 15:20:13 +1200 Subject: [PATCH 05/22] Reinstate TODOs --- interface/src/ui/DialogsManager.cpp | 5 +++++ interface/src/ui/LoginDialog.cpp | 3 +++ 2 files changed, 8 insertions(+) diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index c61d4c4069..9cad1b8a9e 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -144,6 +144,11 @@ void DialogsManager::showDomainLoginDialog() { LoginDialog::showWithSelection(); } +// #######: TODO: Domain version of toggleLoginDialog()? + +// #######: TODO: Domain version of hiadLoginDialog()? + + void DialogsManager::showUpdateDialog() { UpdateDialog::show(); } diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index d64ebdf42a..cd686719ea 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -146,6 +146,9 @@ void LoginDialog::login(const QString& username, const QString& password) const void LoginDialog::loginDomain(const QString& username, const QString& password, const QString& domainAuthProvider) const { qDebug() << "Attempting to login" << username << "into a domain through" << domainAuthProvider; DependencyManager::get()->requestAccessToken(username, password, domainAuthProvider); + + // ####### TODO: It may not be necessary to pass domainAuthProvider to the login dialog and through to here because it was + // originally provided to the QML from C++. } void LoginDialog::loginThroughOculus() { From 56ba137ee3b4b4b81500ce41d89b6d5c07c7b908 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 31 Jul 2020 20:48:27 +1200 Subject: [PATCH 06/22] Get OAuth2 URL from server settings --- domain-server/resources/describe-settings.json | 2 +- domain-server/src/DomainGatekeeper.cpp | 18 ++++++++++-------- .../qml/LoginDialog/LinkAccountBody.qml | 3 +-- interface/src/ui/DialogsManager.cpp | 10 ---------- interface/src/ui/DialogsManager.h | 2 -- interface/src/ui/LoginDialog.cpp | 13 +++---------- interface/src/ui/LoginDialog.h | 3 +-- .../networking/src/DomainAccountManager.cpp | 14 ++++++++++++-- .../networking/src/DomainAccountManager.h | 7 ++++++- libraries/networking/src/DomainHandler.cpp | 5 ++++- 10 files changed, 38 insertions(+), 39 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 281a0be9cc..14c15e1e3d 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -85,7 +85,7 @@ "backup": false }, { - "name": "authentication_oauth2_url_base", + "name": "oauth2_url_base", "label": "Authentication URL Base", "help": "The URL base that the Interface and domain-server will use to make API requests.", "advanced": true diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 32b02382da..f41c20e245 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -444,6 +444,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo return newNode; } +const QString AUTHENTICATION_OAUTH2_URL_BASE = "authentication.oauth2_url_base"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; @@ -533,8 +534,14 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { if (domainHasLogin()) { + QString domainAuthURL; + auto domainAuthURLVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_BASE); + if (domainAuthURLVariant.canConvert()) { + domainAuthURL = domainAuthURLVariant.toString(); + qDebug() << "Domain authorization URL:" << domainAuthURL; + } sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", - nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain); + nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain, domainAuthURL); } else { sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedMetaverse); @@ -1164,13 +1171,8 @@ void DomainGatekeeper::refreshGroupsCache() { } bool DomainGatekeeper::domainHasLogin() { - // The domain may have its own users and groups. This is enabled in the server settings by ... - // ####### TODO: Use a particular string in the server name or set a particular tag in the server's settings? - // Or add a new server setting? - - // ####### TODO: Also configure URL for getting user's group memberships, in the server's settings? - - // ####### TODO + // The domain may have its own users and groups. This is enabled in the server settings by ... ####### + // ####### TODO: Base on server settings. return true; } diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 6f437bb991..3d715d1a39 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -46,7 +46,6 @@ Item { readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() readonly property bool isLoggingInToDomain: loginDialog.getDomainLoginRequested() - readonly property string domainAuthProvider: loginDialog.getDomainLoginAuthProvider() QtObject { id: d @@ -76,7 +75,7 @@ Item { if (!isLoggingInToDomain) { loginDialog.login(emailField.text, passwordField.text); } else { - loginDialog.loginDomain(emailField.text, passwordField.text, domainAuthProvider); + loginDialog.loginDomain(emailField.text, passwordField.text); } if (linkAccountBody.loginDialogPoppedUp) { diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 9cad1b8a9e..848663967f 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -29,7 +29,6 @@ #include "OctreeStatsDialog.h" #include "PreferencesDialog.h" #include "UpdateDialog.h" -#include "DomainHandler.h" #include "scripting/HMDScriptingInterface.h" @@ -131,15 +130,6 @@ void DialogsManager::hideLoginDialog() { void DialogsManager::showDomainLoginDialog() { - const QJsonObject& settingsObject = DependencyManager::get()->getDomainHandler().getSettingsObject(); - static const QString WP_OAUTH2_SERVER_URL = "authentication_oauth2_url_base"; - - if (!settingsObject.contains(WP_OAUTH2_SERVER_URL)) { - qDebug() << "Cannot log in to domain because an OAuth2 authorization was required but no authorization server was specified."; - return; - } - - _domainLoginAuthProvider = settingsObject[WP_OAUTH2_SERVER_URL].toString(); _isDomainLogin = true; LoginDialog::showWithSelection(); } diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index b76ff69386..30127ced68 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -42,7 +42,6 @@ public: void emitAddressBarShown(bool visible) { emit addressBarShown(visible); } void setAddressBarVisible(bool addressBarVisible); bool getIsDomainLogin() { return _isDomainLogin; } - QString getDomainLoginAuthProvider() { return _domainLoginAuthProvider; } public slots: void showAddressBar(); @@ -88,7 +87,6 @@ private: bool _addressBarVisible { false }; bool _isDomainLogin { false }; - QString _domainLoginAuthProvider { "" }; }; #endif // hifi_DialogsManager_h diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index cd686719ea..3cc37bcadb 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -143,12 +143,9 @@ void LoginDialog::login(const QString& username, const QString& password) const DependencyManager::get()->requestAccessToken(username, password); } -void LoginDialog::loginDomain(const QString& username, const QString& password, const QString& domainAuthProvider) const { - qDebug() << "Attempting to login" << username << "into a domain through" << domainAuthProvider; - DependencyManager::get()->requestAccessToken(username, password, domainAuthProvider); - - // ####### TODO: It may not be necessary to pass domainAuthProvider to the login dialog and through to here because it was - // originally provided to the QML from C++. +void LoginDialog::loginDomain(const QString& username, const QString& password) const { + qDebug() << "Attempting to login" << username << "into a domain"; + DependencyManager::get()->requestAccessToken(username, password); } void LoginDialog::loginThroughOculus() { @@ -430,7 +427,3 @@ void LoginDialog::signupFailed(QNetworkReply* reply) { bool LoginDialog::getDomainLoginRequested() const { return DependencyManager::get()->getIsDomainLogin(); } - -QString LoginDialog::getDomainLoginAuthProvider() const { - return DependencyManager::get()->getDomainLoginAuthProvider(); -} diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 49d5dc8fac..310a5db255 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -72,7 +72,7 @@ protected slots: Q_INVOKABLE QString oculusUserID() const; Q_INVOKABLE void login(const QString& username, const QString& password) const; - Q_INVOKABLE void loginDomain(const QString& username, const QString& password, const QString& domainAuthProvider) const; + Q_INVOKABLE void loginDomain(const QString& username, const QString& password) const; Q_INVOKABLE void loginThroughSteam(); Q_INVOKABLE void linkSteam(); Q_INVOKABLE void createAccountFromSteam(QString username = QString()); @@ -85,7 +85,6 @@ protected slots: Q_INVOKABLE bool getLoginDialogPoppedUp() const; Q_INVOKABLE bool getDomainLoginRequested() const; - Q_INVOKABLE QString getDomainLoginAuthProvider() const; }; diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 524a7c4b0a..78ae779fca 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -45,7 +45,17 @@ DomainAccountManager::DomainAccountManager() { connect(this, &DomainAccountManager::loginComplete, this, &DomainAccountManager::sendInterfaceAccessTokenToServer); } -void DomainAccountManager::requestAccessToken(const QString& login, const QString& password, const QString& domainAuthProvider) { +void DomainAccountManager::setAuthURL(const QUrl& authURL) { + if (_authURL != authURL) { + _authURL = authURL; + + qCDebug(networking) << "AccountManager URL for authenticated requests has been changed to" << qPrintable(_authURL.toString()); + + // ####### TODO: See AccountManager::setAuthURL(). + } +} + +void DomainAccountManager::requestAccessToken(const QString& login, const QString& password) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); @@ -53,7 +63,7 @@ void DomainAccountManager::requestAccessToken(const QString& login, const QStrin request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); - _domainAuthProviderURL = domainAuthProvider; + _domainAuthProviderURL = _authURL; _domainAuthProviderURL.setPath("/oauth/token"); QByteArray postData; diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index 4bb197175e..eb0f2659dd 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -13,6 +13,7 @@ #define hifi_DomainAccountManager_h #include +#include #include @@ -22,10 +23,12 @@ class DomainAccountManager : public QObject, public Dependency { public: DomainAccountManager(); + void setAuthURL(const QUrl& authURL); + Q_INVOKABLE bool checkAndSignalForAccessToken(); public slots: - void requestAccessToken(const QString& login, const QString& password, const QString& domainAuthProvider); + void requestAccessToken(const QString& login, const QString& password); void requestAccessTokenFinished(); signals: @@ -41,6 +44,8 @@ private: bool accessTokenIsExpired(); void setAccessTokenFromJSON(const QJsonObject&); void sendInterfaceAccessTokenToServer(); + + QUrl _authURL; }; #endif // hifi_DomainAccountManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 66d4c58a34..0a61036b96 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -584,8 +584,11 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer(); + if (!extraInfo.isEmpty()) { + accountManager->setAuthURL(extraInfo); + } if (!_hasCheckedForDomainAccessToken) { accountManager->checkAndSignalForAccessToken(); From aff327520760c7c40bc59f77e5fb18aa31d7b721 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 1 Aug 2020 10:17:27 +1200 Subject: [PATCH 07/22] Initial OAuth2 login working --- .../networking/src/DomainAccountManager.cpp | 72 ++++++++++++------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 78ae779fca..d36f4b5f09 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -29,17 +29,19 @@ #include "udt/PacketHeaders.h" #include "NetworkAccessManager.h" -const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; -const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner"; +// FIXME: Generalize to other OAuth2 sources for domain login. +const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; +const QString DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE = "foo bar"; // ####### TODO: WordPress plugin's required scope. +// ####### TODO: Should scope be configured in domain server settings? + +// ####### TODO: Add storing domain URL and check against it when retrieving values? +// ####### TODO: Add storing _authURL and check against it when retrieving values? Setting::Handle domainAccessToken {"private/domainAccessToken", "" }; Setting::Handle domainAccessRefreshToken {"private/domainAccessToken", "" }; Setting::Handle domainAccessTokenExpiresIn {"private/domainAccessTokenExpiresIn", -1 }; Setting::Handle domainAccessTokenType {"private/domainAccessTokenType", "" }; -QUrl _domainAuthProviderURL; - -// FIXME: If you try to authenticate this way on another domain, no one knows what will happen. Probably death. DomainAccountManager::DomainAccountManager() { connect(this, &DomainAccountManager::loginComplete, this, &DomainAccountManager::sendInterfaceAccessTokenToServer); @@ -57,25 +59,29 @@ void DomainAccountManager::setAuthURL(const QUrl& authURL) { void DomainAccountManager::requestAccessToken(const QString& login, const QString& password) { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest request; - request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); - - _domainAuthProviderURL = _authURL; - _domainAuthProviderURL.setPath("/oauth/token"); - - QByteArray postData; - postData.append("grant_type=password&"); - postData.append("username=" + QUrl::toPercentEncoding(login) + "&"); - postData.append("password=" + QUrl::toPercentEncoding(password) + "&"); - postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); - - request.setUrl(_domainAuthProviderURL); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + // ####### TODO: WordPress plugin's authorization requirements. + request.setRawHeader(QByteArray("Authorization"), QByteArray("Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x")); - QNetworkReply* requestReply = networkAccessManager.post(request, postData); + QByteArray formData; + formData.append("grant_type=password&"); + formData.append("username=" + QUrl::toPercentEncoding(login) + "&"); + formData.append("password=" + QUrl::toPercentEncoding(password) + "&"); + formData.append("scope=" + DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE); + // ####### TODO: Include state? + + QUrl domainAuthURL = _authURL; + domainAuthURL.setPath("/token"); // ####### TODO: miniOrange-mandated URL. ####### TODO: Should this be included in the server settings value? + request.setUrl(domainAuthURL); + + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkReply* requestReply = networkAccessManager.post(request, formData); connect(requestReply, &QNetworkReply::finished, this, &DomainAccountManager::requestAccessTokenFinished); } @@ -86,19 +92,25 @@ void DomainAccountManager::requestAccessTokenFinished() { const QJsonObject& rootObject = jsonResponse.object(); if (!rootObject.contains("error")) { - // construct an OAuthAccessToken from the json object + // ####### TODO: Process response scope? + // ####### TODO: Process response state? - if (!rootObject.contains("access_token") || !rootObject.contains("expires_in") + if (!rootObject.contains("access_token") + // ####### TODO: Does WordPRess plugin provide "expires_in"? + // If so, handle here, or is it just the domain server that needs to use it? + //|| !rootObject.contains("expires_in") || !rootObject.contains("token_type")) { - // TODO: error handling - malformed token response + qCDebug(networking) << "Received a response for password grant that is missing one or more expected values."; + emit loginFailed(); + } else { + // clear the path from the response URL so we have the right root URL for this access token QUrl rootURL = requestReply->url(); rootURL.setPath(""); qCDebug(networking) << "Storing a domain account with access-token for" << qPrintable(rootURL.toString()); - setAccessTokenFromJSON(rootObject); emit loginComplete(rootURL); @@ -114,7 +126,11 @@ void DomainAccountManager::sendInterfaceAccessTokenToServer() { } bool DomainAccountManager::accessTokenIsExpired() { + // ####### TODO: accessTokenIsExpired() + return true; + /* return domainAccessTokenExpiresIn.get() != -1 && domainAccessTokenExpiresIn.get() <= QDateTime::currentMSecsSinceEpoch(); + */ } @@ -125,7 +141,7 @@ bool DomainAccountManager::hasValidAccessToken() { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qCDebug(networking) << "An access token is required for requests to" - << qPrintable(_domainAuthProviderURL.toString()); + << qPrintable(_authURL.toString()); } return false; @@ -143,15 +159,23 @@ bool DomainAccountManager::hasValidAccessToken() { } void DomainAccountManager::setAccessTokenFromJSON(const QJsonObject& jsonObject) { + // ####### TODO: Enable and use these. + /* domainAccessToken.set(jsonObject["access_token"].toString()); domainAccessRefreshToken.set(jsonObject["refresh_token"].toString()); domainAccessTokenExpiresIn.set(QDateTime::currentMSecsSinceEpoch() + (jsonObject["expires_in"].toDouble() * 1000)); domainAccessTokenType.set(jsonObject["token_type"].toString()); + */ } bool DomainAccountManager::checkAndSignalForAccessToken() { bool hasToken = hasValidAccessToken(); + // ####### TODO: Handle hasToken == true. + // It causes the login dialog not to display (OK) but somewhere the domain server needs to be sent it (and if domain server + // gets error when trying to use it then user should be prompted to login). + hasToken = false; + if (!hasToken) { // Emit a signal so somebody can call back to us and request an access token given a user name and password. From 15c6baceb8ed6a21a4a6a1e741b8c854ebecfb26 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 1 Aug 2020 18:57:21 +1200 Subject: [PATCH 08/22] Send OAuth2 username and tokens to domain server --- domain-server/src/DomainGatekeeper.cpp | 21 ++++++------- domain-server/src/DomainGatekeeper.h | 4 +-- .../networking/src/DomainAccountManager.cpp | 22 +++++++++++--- .../networking/src/DomainAccountManager.h | 10 ++++++- libraries/networking/src/NodeList.cpp | 30 ++++++++++++------- libraries/networking/src/NodeList.h | 2 ++ 6 files changed, 61 insertions(+), 28 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index f41c20e245..a1581c26f1 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -95,7 +95,7 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointersecond); } else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) { QByteArray usernameSignature; - QByteArray domainUsernameSignature; + QString domainTokens; if (message->getBytesLeftToRead() > 0) { // read username from packet @@ -110,14 +110,14 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer> domainUsername; if (message->getBytesLeftToRead() > 0) { - // Read domain signature from packet. - packetStream >> domainUsernameSignature; + // Read domain tokens from packet. + packetStream >> domainTokens; } } } } - node = processAgentConnectRequest(nodeConnection, username, usernameSignature, domainUsername, domainUsernameSignature); + node = processAgentConnectRequest(nodeConnection, username, usernameSignature, domainUsername, domainTokens); } if (node) { @@ -452,7 +452,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, - const QByteArray& domainUsernameSignature) { + const QString& domainTokens) { auto limitedNodeList = DependencyManager::get(); @@ -502,17 +502,18 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect QString verifiedDomainUsername; QStringList verifiedDomainUserGroups; if (domainHasLogin() && !domainUsername.isEmpty()) { - if (domainUsernameSignature.isEmpty()) { + if (domainTokens.isEmpty()) { // User is attempting to prove their domain identity. // ####### TODO: OAuth2 corollary of metaverse code, above. - getDomainGroupMemberships(domainUsernameSignature); // Optimistically get started on group memberships. + // ####### TODO: Do the following now? Probably can't! + //getDomainGroupMemberships(domainUsername); // Optimistically get started on group memberships. #ifdef WANT_DEBUG qDebug() << "stalling login because we have no domain username-signature:" << domainUsername; #endif return SharedNodePointer(); - } else if (verifyDomainUserSignature(domainUsername, domainUsernameSignature, nodeConnection.senderSockAddr)) { + } else if (verifyDomainUserSignature(domainUsername, domainTokens, nodeConnection.senderSockAddr)) { // User's domain identity is confirmed. getDomainGroupMemberships(domainUsername); verifiedDomainUsername = domainUsername.toLower(); @@ -742,10 +743,10 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, } bool DomainGatekeeper::verifyDomainUserSignature(const QString& domainUsername, - const QByteArray& domainUsernameSignature, + const QString& domainTokens, const HifiSockAddr& senderSockAddr) { - // ####### TODO: Verify via domain OAuth2. + // ####### TODO: Verify response from domain OAuth2 request to WordPress, if it's arrived yet. bool success = true; if (success) { return true; diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 99bd875457..0cb757a9ea 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -79,13 +79,13 @@ private: const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, - const QByteArray& domainUsernameSignature); + const QString& domainTokens); SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection); bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); - bool verifyDomainUserSignature(const QString& domainUsername, const QByteArray& domainUsernameSignature, + bool verifyDomainUserSignature(const QString& domainUsername, const QString& domainUsernameSignature, const HifiSockAddr& senderSockAddr); bool isWithinMaxCapacity(); diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index d36f4b5f09..cb0b93232e 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -11,6 +11,8 @@ #include "DomainAccountManager.h" +// ####### TODO: Check that all #includes are still needed. + #include #include @@ -43,7 +45,12 @@ Setting::Handle domainAccessTokenExpiresIn {"private/domainAccessTokenExpir Setting::Handle domainAccessTokenType {"private/domainAccessTokenType", "" }; -DomainAccountManager::DomainAccountManager() { +DomainAccountManager::DomainAccountManager() : + _authURL(), + _username(), + _access_token(), + _refresh_token() +{ connect(this, &DomainAccountManager::loginComplete, this, &DomainAccountManager::sendInterfaceAccessTokenToServer); } @@ -57,8 +64,11 @@ void DomainAccountManager::setAuthURL(const QUrl& authURL) { } } -void DomainAccountManager::requestAccessToken(const QString& login, const QString& password) { +void DomainAccountManager::requestAccessToken(const QString& username, const QString& password) { + _username = username; + _access_token = ""; + _refresh_token = ""; QNetworkRequest request; @@ -69,7 +79,7 @@ void DomainAccountManager::requestAccessToken(const QString& login, const QStrin QByteArray formData; formData.append("grant_type=password&"); - formData.append("username=" + QUrl::toPercentEncoding(login) + "&"); + formData.append("username=" + QUrl::toPercentEncoding(username) + "&"); formData.append("password=" + QUrl::toPercentEncoding(password) + "&"); formData.append("scope=" + DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE); // ####### TODO: Include state? @@ -122,7 +132,7 @@ void DomainAccountManager::requestAccessTokenFinished() { } void DomainAccountManager::sendInterfaceAccessTokenToServer() { - // TODO: Send successful packet to the domain-server. + emit newTokens(); } bool DomainAccountManager::accessTokenIsExpired() { @@ -159,7 +169,11 @@ bool DomainAccountManager::hasValidAccessToken() { } void DomainAccountManager::setAccessTokenFromJSON(const QJsonObject& jsonObject) { + _access_token = jsonObject["access_token"].toString(); + _refresh_token = jsonObject["refresh_token"].toString(); + // ####### TODO: Enable and use these. + // ####### TODO: Protect these per AccountManager? /* domainAccessToken.set(jsonObject["access_token"].toString()); domainAccessRefreshToken.set(jsonObject["refresh_token"].toString()); diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index eb0f2659dd..08b625a246 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -25,10 +25,14 @@ public: void setAuthURL(const QUrl& authURL); + QString getUsername() { return _username; } + QString getAccessToken() { return _access_token; } + QString getRefreshToken() { return _refresh_token; } + Q_INVOKABLE bool checkAndSignalForAccessToken(); public slots: - void requestAccessToken(const QString& login, const QString& password); + void requestAccessToken(const QString& username, const QString& password); void requestAccessTokenFinished(); signals: @@ -36,6 +40,7 @@ signals: void loginComplete(const QUrl& authURL); void loginFailed(); void logoutComplete(); + void newTokens(); private slots: @@ -46,6 +51,9 @@ private: void sendInterfaceAccessTokenToServer(); QUrl _authURL; + QString _username; // ####### TODO: Store elsewhere? + QString _access_token; // ####... "" + QString _refresh_token; // ####... "" }; #endif // hifi_DomainAccountManager_h diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 262ad0d2a4..16eba6c6a1 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -34,6 +34,7 @@ #include "AddressManager.h" #include "Assignment.h" #include "AudioHelpers.h" +#include "DomainAccountManager.h" #include "HifiSockAddr.h" #include "FingerprintUtils.h" @@ -104,6 +105,13 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) // clear our NodeList when logout is requested connect(accountManager.data(), &AccountManager::logoutComplete , this, [this]{ reset("Logged out"); }); + // Only used in Interface. + auto domainAccountManager = DependencyManager::get(); + if (domainAccountManager) { + _hasDomainAccountManager = true; + connect(domainAccountManager.data(), &DomainAccountManager::newTokens, this, &NodeList::sendDomainServerCheckIn); + } + // anytime we get a new node we will want to attempt to punch to it connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::startNodeHolePunch); connect(this, &LimitedNodeList::nodeSocketUpdated, this, &NodeList::startNodeHolePunch); @@ -468,6 +476,7 @@ void NodeList::sendDomainServerCheckIn() { packetStream << _ownerType.load() << publicSockAddr << localSockAddr << _nodeTypesOfInterest.toList(); packetStream << DependencyManager::get()->getPlaceName(); + // ####### TODO: Also send if need to send new domainLogin data? if (!domainIsConnected) { DataServerAccountInfo& accountInfo = accountManager->getAccountInfo(); packetStream << accountInfo.getUsername(); @@ -477,21 +486,20 @@ void NodeList::sendDomainServerCheckIn() { const QByteArray& usernameSignature = accountManager->getAccountInfo().getUsernameSignature(connectionToken); packetStream << usernameSignature; } else { - packetStream << QString(""); // Placeholder in case have domainUsername. + // ####### TODO: Only append if are going to send domain username? + packetStream << QString(""); // Placeholder in case have domain username. } } else { - packetStream << QString(""); // Placeholder in case have domainUsername. + // ####### TODO: Only append if are going to send domainUsername? + packetStream << QString("") << QString(""); // Placeholders in case have domain username. } - // ####### TODO: Send domain username and signature if domain has these and aren't logged in. - // ####### If get into difficulties, could perhaps send domain's username and signature instead of metaverse. - bool domainLoginIsConnected = false; - if (!domainLoginIsConnected) { - if (false) { // ####### For testing, false causes user to be considered "not logged in". - packetStream << QString("a@b.c"); - if (true) { // ####### For testing, false is unhandled at this stage. - packetStream << QString("signature"); // #######: Consider "logged in" if this is sent during testing. - } + // Send domain domain login data from Interface to domain server. + if (_hasDomainAccountManager) { + auto domainAccountManager = DependencyManager::get(); + if (!domainAccountManager->getUsername().isEmpty()) { + packetStream << domainAccountManager->getUsername(); + packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken()); } } diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index c377ea89cb..4954c53c84 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -196,6 +196,8 @@ private: #if (PR_BUILD || DEV_BUILD) bool _shouldSendNewerVersion { false }; #endif + + bool _hasDomainAccountManager { false }; }; #endif // hifi_NodeList_h From 8cdd76a42e1b1d3562050531590db419b0f1eda9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 2 Aug 2020 16:17:04 +1200 Subject: [PATCH 09/22] Verify user at domain server --- domain-server/src/DomainGatekeeper.cpp | 163 ++++++++++++++---- domain-server/src/DomainGatekeeper.h | 26 ++- .../networking/src/DomainAccountManager.cpp | 3 + libraries/networking/src/DomainHandler.h | 1 + libraries/networking/src/NodeList.cpp | 3 +- 5 files changed, 157 insertions(+), 39 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index a1581c26f1..210dabece1 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -90,12 +90,13 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointersecond); } else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) { QByteArray usernameSignature; - QString domainTokens; + + QString domainUsername; + QStringList domainTokens; if (message->getBytesLeftToRead() > 0) { // read username from packet @@ -111,13 +112,17 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetBytesLeftToRead() > 0) { // Read domain tokens from packet. - packetStream >> domainTokens; + + QString domainTokensString; + packetStream >> domainTokensString; + domainTokens = domainTokensString.split(":"); } } } } - node = processAgentConnectRequest(nodeConnection, username, usernameSignature, domainUsername, domainTokens); + node = processAgentConnectRequest(nodeConnection, username, usernameSignature, + domainUsername, domainTokens.value(0), domainTokens.value(1)); } if (node) { @@ -452,7 +457,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, - const QString& domainTokens) { + const QString& domainAccessToken, + const QString& domainRefreshToken) { auto limitedNodeList = DependencyManager::get(); @@ -502,30 +508,39 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect QString verifiedDomainUsername; QStringList verifiedDomainUserGroups; if (domainHasLogin() && !domainUsername.isEmpty()) { - if (domainTokens.isEmpty()) { + + if (domainAccessToken.isEmpty()) { // User is attempting to prove their domain identity. - - // ####### TODO: OAuth2 corollary of metaverse code, above. - - // ####### TODO: Do the following now? Probably can't! - //getDomainGroupMemberships(domainUsername); // Optimistically get started on group memberships. #ifdef WANT_DEBUG - qDebug() << "stalling login because we have no domain username-signature:" << domainUsername; + qDebug() << "Stalling login because we have no domain OAuth2 tokens:" << domainUsername; #endif return SharedNodePointer(); - } else if (verifyDomainUserSignature(domainUsername, domainTokens, nodeConnection.senderSockAddr)) { + + } else if (!_verifiedDomainUserIdentities.contains(domainUsername) + || _verifiedDomainUserIdentities[domainUsername] != QPair(domainAccessToken, domainRefreshToken)) { + // ####### TODO: Write a function for the above test. + // User's domain identity needs to be confirmed. + if (_verifiedDomainUserIdentities.contains(domainUsername)) { + _verifiedDomainUserIdentities.remove(domainUsername); + } + requestDomainUser(domainUsername, domainAccessToken, domainRefreshToken); +#ifdef WANT_DEBUG + qDebug() << "Stalling login because we haven't authenticated user yet:" << domainUsername; +#endif + + } else if (verifyDomainUserSignature(domainUsername, domainAccessToken, domainRefreshToken, + nodeConnection.senderSockAddr)) { // User's domain identity is confirmed. getDomainGroupMemberships(domainUsername); verifiedDomainUsername = domainUsername.toLower(); + } else { - // User's identity didn't check out. - - // ####### TODO: OAuth2 corollary of metaverse code, above. - + // User's domain identity didn't check out. #ifdef WANT_DEBUG - qDebug() << "stalling login because domain signature verification failed:" << domainUsername; + qDebug() << "Stalling login because domain user verification failed:" << domainUsername; #endif return SharedNodePointer(); + } } @@ -742,17 +757,17 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, return false; } -bool DomainGatekeeper::verifyDomainUserSignature(const QString& domainUsername, - const QString& domainTokens, - const HifiSockAddr& senderSockAddr) { +// ####### TODO: Rename to verifyDomainUser()? +bool DomainGatekeeper::verifyDomainUserSignature(const QString& username, const QString& accessToken, + const QString& refreshToken, const HifiSockAddr& senderSockAddr) { // ####### TODO: Verify response from domain OAuth2 request to WordPress, if it's arrived yet. - bool success = true; - if (success) { + // #### Or assume the verification step has already occurred? + if (_verifiedDomainUserIdentities.contains(username)) { return true; } - sendConnectionDeniedPacket("Error decrypting domain username signature.", senderSockAddr, + sendConnectionDeniedPacket("Error verifying domain user.", senderSockAddr, DomainHandler::ConnectionRefusedReason::LoginErrorDomain); return false; } @@ -1171,12 +1186,6 @@ void DomainGatekeeper::refreshGroupsCache() { #endif } -bool DomainGatekeeper::domainHasLogin() { - // The domain may have its own users and groups. This is enabled in the server settings by ... ####### - // ####### TODO: Base on server settings. - return true; -} - void DomainGatekeeper::initLocalIDManagement() { std::uniform_int_distribution sixteenBitRand; std::random_device randomDevice; @@ -1204,3 +1213,97 @@ Node::LocalID DomainGatekeeper::findOrCreateLocalID(const QUuid& uuid) { _localIDs.insert(newLocalID); return newLocalID; } + + +bool DomainGatekeeper::domainHasLogin() { + // The domain may have its own users and groups. This is enabled in the server settings by ... ####### + // ####### TODO: Base on server settings. + return true; +} + +void DomainGatekeeper::requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken) { + + // ####### TODO: Move this further up the chain such that generates "invalid username or password" condition? + // Don't request identity for the standard psuedo-account-names. + if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) { + return; + } + + if (_inFlightDomainUserIdentityRequests.contains(username)) { + // Domain identify request for this username is already flight. + return; + } + _inFlightDomainUserIdentityRequests.insert(username, QPair(accessToken, refreshToken)); + + QString API_BASE = "http://127.0.0.1:9001/wp-json/"; + // Typically "http://oursite.com/wp-json/". + // However, if using non-pretty permalinks or otherwise get a 404 error then use "http://oursite.com/?rest_route=/". + + // ####### TODO: Confirm API w.r.t. OAuth2 plugin's capabilities. + // Get data pertaining to "me", the user who generated the access token. + QString API_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; + + // ####### TODO: Append a random key to check in response? + + QNetworkRequest request; + + request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + // ####### TODO: WordPress plugin's authorization requirements. + request.setRawHeader(QByteArray("Authorization"), QString("Bearer " + accessToken).toUtf8()); + + QByteArray formData; // No data to send. + + QUrl domainUserURL = API_BASE + API_ROUTE; + domainUserURL = "http://localhost:9002/resource"; // ####### TODO: Delete + request.setUrl(domainUserURL); + + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + + // ####### TODO: Handle invalid URL (e.g., set timeout or similar). + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkReply* requestReply = networkAccessManager.post(request, formData); + connect(requestReply, &QNetworkReply::finished, this, &DomainGatekeeper::requestDomainUserFinished); +} + +void DomainGatekeeper::requestDomainUserFinished() { + + QNetworkReply* requestReply = reinterpret_cast(sender()); + + auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (200 <= httpStatus && httpStatus < 300) { + // Success. + QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); + const QJsonObject& rootObject = jsonResponse.object(); + // ####### Expected response: + /* + { + id: 2, + username : 'apiuser', + roles : ['subscriber'] , + } + */ + + // ####### TODO: Handle invalid / unexpected response. + + QString username = rootObject["username"].toString().toLower(); + // ####### TODO: Handle invalid username or one that isn't in the _inFlight list. + + if (_inFlightDomainUserIdentityRequests.contains(username)) { + // Success! Verified user. + _verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username)); + _inFlightDomainUserIdentityRequests.remove(username); + } else { + // Unexpected response. + // ####### TODO + } + + } else { + // Failure. + // ####### TODO: Is this the best way to handle _inFlightDomainUserIdentityRequests? + // If there's a brief network glitch will it recover? + // Perhaps clear on a timer? Cancel timer upon subsequent successful responses? + _inFlightDomainUserIdentityRequests.clear(); + } +} diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 0cb757a9ea..eaf20a6285 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -72,6 +72,10 @@ public slots: private slots: void handlePeerPingTimeout(); + + // Login and groups for domain, separate from metaverse. + void requestDomainUserFinished(); + private: SharedNodePointer processAssignmentConnectRequest(const NodeConnectionData& nodeConnection, const PendingAssignedNodeData& pendingAssignment); @@ -79,13 +83,14 @@ private: const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, - const QString& domainTokens); + const QString& domainAccessToken, + const QString& domainRefreshToken); SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection); bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); - bool verifyDomainUserSignature(const QString& domainUsername, const QString& domainUsernameSignature, + bool verifyDomainUserSignature(const QString& username, const QString& accessToken, const QString& refreshToken, const HifiSockAddr& senderSockAddr); bool isWithinMaxCapacity(); @@ -135,20 +140,25 @@ private: // void getIsGroupMember(const QString& username, const QUuid groupID); void getDomainOwnerFriendsList(); - // Login and groups for domain, separate from metaverse. - bool domainHasLogin(); - void getDomainGroupMemberships(const QString& domainUserName); - QHash _domainGroupMemberships; // - // Local ID management. void initLocalIDManagement(); using UUIDToLocalID = std::unordered_map ; using LocalIDs = std::unordered_set; LocalIDs _localIDs; UUIDToLocalID _uuidToLocalID; - Node::LocalID _currentLocalID; Node::LocalID _idIncrement; + + // Login and groups for domain, separate from metaverse. + bool domainHasLogin(); + void requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken); + + typedef QHash> DomainUserIdentities; // > + DomainUserIdentities _inFlightDomainUserIdentityRequests; // Keep track of domain user identity requests in progress. + DomainUserIdentities _verifiedDomainUserIdentities; // Verified domain users. + + void getDomainGroupMemberships(const QString& domainUserName); + QHash _domainGroupMemberships; // }; diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index cb0b93232e..7fc02893eb 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -101,9 +101,12 @@ void DomainAccountManager::requestAccessTokenFinished() { QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); const QJsonObject& rootObject = jsonResponse.object(); + // ####### TODO: Test HTTP response codes rather than object contains "error". + // #### reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200 if (!rootObject.contains("error")) { // ####### TODO: Process response scope? // ####### TODO: Process response state? + // ####### TODO: Check that token type == "Bearer"? if (!rootObject.contains("access_token") // ####### TODO: Does WordPRess plugin provide "expires_in"? diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index ff252426a9..d0bd40f7a7 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -125,6 +125,7 @@ public: bool isConnected() const { return _isConnected; } void setIsConnected(bool isConnected); + bool isServerless() const { return _domainURL.scheme() != URL_SCHEME_HIFI; } bool getInterstitialModeEnabled() const; void setInterstitialModeEnabled(bool enableInterstitialMode); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 16eba6c6a1..539d044c1e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -499,7 +499,8 @@ void NodeList::sendDomainServerCheckIn() { auto domainAccountManager = DependencyManager::get(); if (!domainAccountManager->getUsername().isEmpty()) { packetStream << domainAccountManager->getUsername(); - packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken()); + if (!domainAccountManager->getAccessToken().isEmpty()) { + packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken()); } } From c3769a5f7426855d8acfb2e10a6af469b6ad9fc1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 2 Aug 2020 17:33:13 +1200 Subject: [PATCH 10/22] Get WordPress API path from domain server settings --- domain-server/resources/describe-settings.json | 8 +++++++- domain-server/src/DomainGatekeeper.cpp | 15 ++++++++------- libraries/networking/src/NodeList.cpp | 1 + 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 14c15e1e3d..ba019e3fe7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -87,7 +87,13 @@ { "name": "oauth2_url_base", "label": "Authentication URL Base", - "help": "The URL base that the Interface and domain-server will use to make API requests.", + "help": "The URL base that the Interface will use to login via OAuth2.", + "advanced": true + }, + { + "name": "wordpress_url_base", + "label": "WordPress API URL Base", + "help": "The URL base that the domain server will use to make WordPress API calls. Typically \"http://oursite.com/wp-json/\". However, if using non-pretty permalinks or otherwise get a 404 error then use \"http://oursite.com/?rest_route=/\".", "advanced": true } ] diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 210dabece1..29667b0fd5 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -450,6 +450,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo } const QString AUTHENTICATION_OAUTH2_URL_BASE = "authentication.oauth2_url_base"; +const QString AUTHENTICATION_WORDPRESS_URL_BASE = "authentication.wordpress_url_base"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; @@ -1235,13 +1236,15 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& } _inFlightDomainUserIdentityRequests.insert(username, QPair(accessToken, refreshToken)); - QString API_BASE = "http://127.0.0.1:9001/wp-json/"; - // Typically "http://oursite.com/wp-json/". - // However, if using non-pretty permalinks or otherwise get a 404 error then use "http://oursite.com/?rest_route=/". + QString apiBase = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString(); + if (!apiBase.endsWith("/")) { + apiBase += "/"; + } - // ####### TODO: Confirm API w.r.t. OAuth2 plugin's capabilities. // Get data pertaining to "me", the user who generated the access token. - QString API_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; + // ####### TODO: Confirm API_ROUTE w.r.t. OAuth2 plugin's capabilities. + QString API_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; + QUrl domainUserURL = apiBase + API_ROUTE; // ####### TODO: Append a random key to check in response? @@ -1254,8 +1257,6 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& QByteArray formData; // No data to send. - QUrl domainUserURL = API_BASE + API_ROUTE; - domainUserURL = "http://localhost:9002/resource"; // ####### TODO: Delete request.setUrl(domainUserURL); request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 539d044c1e..e02a8dd56e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -501,6 +501,7 @@ void NodeList::sendDomainServerCheckIn() { packetStream << domainAccountManager->getUsername(); if (!domainAccountManager->getAccessToken().isEmpty()) { packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken()); + } } } From fdb4a5605a58a5e4c0d76325e4467e64195da192 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 2 Aug 2020 20:54:09 +1200 Subject: [PATCH 11/22] Tidy processing user connect request --- domain-server/src/DomainGatekeeper.cpp | 31 ++++++++++++++------------ domain-server/src/DomainGatekeeper.h | 5 +++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 29667b0fd5..ee8da95d6c 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -517,20 +517,15 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect #endif return SharedNodePointer(); - } else if (!_verifiedDomainUserIdentities.contains(domainUsername) - || _verifiedDomainUserIdentities[domainUsername] != QPair(domainAccessToken, domainRefreshToken)) { - // ####### TODO: Write a function for the above test. + } else if (needToVerifyDomainUserIdentity(domainUsername, domainAccessToken, domainRefreshToken)) { // User's domain identity needs to be confirmed. - if (_verifiedDomainUserIdentities.contains(domainUsername)) { - _verifiedDomainUserIdentities.remove(domainUsername); - } requestDomainUser(domainUsername, domainAccessToken, domainRefreshToken); #ifdef WANT_DEBUG qDebug() << "Stalling login because we haven't authenticated user yet:" << domainUsername; #endif - } else if (verifyDomainUserSignature(domainUsername, domainAccessToken, domainRefreshToken, - nodeConnection.senderSockAddr)) { + } else if (verifyDomainUserIdentity(domainUsername, domainAccessToken, domainRefreshToken, + nodeConnection.senderSockAddr)) { // User's domain identity is confirmed. getDomainGroupMemberships(domainUsername); verifiedDomainUsername = domainUsername.toLower(); @@ -758,13 +753,17 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, return false; } -// ####### TODO: Rename to verifyDomainUser()? -bool DomainGatekeeper::verifyDomainUserSignature(const QString& username, const QString& accessToken, - const QString& refreshToken, const HifiSockAddr& senderSockAddr) { - // ####### TODO: Verify response from domain OAuth2 request to WordPress, if it's arrived yet. - // #### Or assume the verification step has already occurred? - if (_verifiedDomainUserIdentities.contains(username)) { +bool DomainGatekeeper::needToVerifyDomainUserIdentity(const QString& username, const QString& accessToken, + const QString& refreshToken) { + return !_verifiedDomainUserIdentities.contains(username) + || _verifiedDomainUserIdentities.value(username) != QPair(accessToken, refreshToken); +} + +bool DomainGatekeeper::verifyDomainUserIdentity(const QString& username, const QString& accessToken, + const QString& refreshToken, const HifiSockAddr& senderSockAddr) { + if (_verifiedDomainUserIdentities.contains(username) + && _verifiedDomainUserIdentities.value(username) == QPair(accessToken, refreshToken)) { return true; } @@ -1236,6 +1235,10 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& } _inFlightDomainUserIdentityRequests.insert(username, QPair(accessToken, refreshToken)); + if (_verifiedDomainUserIdentities.contains(username)) { + _verifiedDomainUserIdentities.remove(username); + } + QString apiBase = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString(); if (!apiBase.endsWith("/")) { apiBase += "/"; diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index eaf20a6285..263d5b853d 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -90,8 +90,9 @@ private: bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); - bool verifyDomainUserSignature(const QString& username, const QString& accessToken, const QString& refreshToken, - const HifiSockAddr& senderSockAddr); + bool needToVerifyDomainUserIdentity(const QString& username, const QString& accessToken, const QString& refreshToken); + bool verifyDomainUserIdentity(const QString& username, const QString& accessToken, const QString& refreshToken, + const HifiSockAddr& senderSockAddr); bool isWithinMaxCapacity(); From dfbad4efc097fd4a9c930034822ea3ac707d5c11 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 2 Aug 2020 21:22:59 +1200 Subject: [PATCH 12/22] Use "enable domain authentication" server setting --- domain-server/resources/describe-settings.json | 2 +- domain-server/src/DomainGatekeeper.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index ba019e3fe7..2593740969 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -93,7 +93,7 @@ { "name": "wordpress_url_base", "label": "WordPress API URL Base", - "help": "The URL base that the domain server will use to make WordPress API calls. Typically \"http://oursite.com/wp-json/\". However, if using non-pretty permalinks or otherwise get a 404 error then use \"http://oursite.com/?rest_route=/\".", + "help": "The URL base that the domain server will use to make WordPress API calls. Typically \"https://oursite.com/wp-json/\". However, if using non-pretty permalinks or otherwise get a 404 error then try \"https://oursite.com/?rest_route=/\".", "advanced": true } ] diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index ee8da95d6c..6227ddb634 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -449,6 +449,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo return newNode; } +const QString AUTHENTICATION_ENAABLED = "authentication.enable_oauth2"; const QString AUTHENTICATION_OAUTH2_URL_BASE = "authentication.oauth2_url_base"; const QString AUTHENTICATION_WORDPRESS_URL_BASE = "authentication.wordpress_url_base"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; @@ -1216,9 +1217,11 @@ Node::LocalID DomainGatekeeper::findOrCreateLocalID(const QUuid& uuid) { bool DomainGatekeeper::domainHasLogin() { - // The domain may have its own users and groups. This is enabled in the server settings by ... ####### - // ####### TODO: Base on server settings. - return true; + // The domain may have its own users and groups in a WordPress site. + // ####### TODO: Add checks of any further domain server settings used. + return _server->_settingsManager.valueForKeyPath(AUTHENTICATION_ENAABLED).toBool() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_BASE).toString().isEmpty() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString().isEmpty(); } void DomainGatekeeper::requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken) { From 43b6e77235afe395ac3684419a86064a3710504c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 3 Aug 2020 09:17:12 +1200 Subject: [PATCH 13/22] Tidy networking code --- .../resources/describe-settings.json | 6 +-- domain-server/src/DomainGatekeeper.cpp | 37 ++++++------- domain-server/src/DomainGatekeeper.h | 2 +- .../networking/src/DomainAccountManager.cpp | 54 ++++++++++--------- .../networking/src/DomainAccountManager.h | 4 +- 5 files changed, 54 insertions(+), 49 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 2593740969..5560fdba56 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -85,9 +85,9 @@ "backup": false }, { - "name": "oauth2_url_base", - "label": "Authentication URL Base", - "help": "The URL base that the Interface will use to login via OAuth2.", + "name": "oauth2_url_path", + "label": "Authentication URL", + "help": "The URL that the Interface will use to login via OAuth2.", "advanced": true }, { diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 6227ddb634..16c7383bf9 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -450,7 +450,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo } const QString AUTHENTICATION_ENAABLED = "authentication.enable_oauth2"; -const QString AUTHENTICATION_OAUTH2_URL_BASE = "authentication.oauth2_url_base"; +const QString AUTHENTICATION_OAUTH2_URL_PATH = "authentication.oauth2_url_path"; const QString AUTHENTICATION_WORDPRESS_URL_BASE = "authentication.wordpress_url_base"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; @@ -548,10 +548,9 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { if (domainHasLogin()) { QString domainAuthURL; - auto domainAuthURLVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_BASE); + auto domainAuthURLVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_PATH); if (domainAuthURLVariant.canConvert()) { domainAuthURL = domainAuthURLVariant.toString(); - qDebug() << "Domain authorization URL:" << domainAuthURL; } sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain, domainAuthURL); @@ -1220,7 +1219,7 @@ bool DomainGatekeeper::domainHasLogin() { // The domain may have its own users and groups in a WordPress site. // ####### TODO: Add checks of any further domain server settings used. return _server->_settingsManager.valueForKeyPath(AUTHENTICATION_ENAABLED).toBool() - && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_BASE).toString().isEmpty() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_PATH).toString().isEmpty() && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString().isEmpty(); } @@ -1233,7 +1232,7 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& } if (_inFlightDomainUserIdentityRequests.contains(username)) { - // Domain identify request for this username is already flight. + // Domain identify request for this username is already in progress. return; } _inFlightDomainUserIdentityRequests.insert(username, QPair(accessToken, refreshToken)); @@ -1248,9 +1247,9 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& } // Get data pertaining to "me", the user who generated the access token. - // ####### TODO: Confirm API_ROUTE w.r.t. OAuth2 plugin's capabilities. - QString API_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; - QUrl domainUserURL = apiBase + API_ROUTE; + // ####### TODO: Confirm API route and data w.r.t. OAuth2 plugin's capabilities. [plugin] + const QString WORDPRESS_USER_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; + QUrl domainUserURL = apiBase + WORDPRESS_USER_ROUTE; // ####### TODO: Append a random key to check in response? @@ -1258,7 +1257,6 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - // ####### TODO: WordPress plugin's authorization requirements. request.setRawHeader(QByteArray("Authorization"), QString("Bearer " + accessToken).toUtf8()); QByteArray formData; // No data to send. @@ -1278,12 +1276,13 @@ void DomainGatekeeper::requestDomainUserFinished() { QNetworkReply* requestReply = reinterpret_cast(sender()); + QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); + const QJsonObject& rootObject = jsonResponse.object(); + auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (200 <= httpStatus && httpStatus < 300) { // Success. - QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); - const QJsonObject& rootObject = jsonResponse.object(); - // ####### Expected response: + // ####### TODO: Verify Expected response. [plugin] /* { id: 2, @@ -1292,22 +1291,24 @@ void DomainGatekeeper::requestDomainUserFinished() { } */ - // ####### TODO: Handle invalid / unexpected response. - QString username = rootObject["username"].toString().toLower(); - // ####### TODO: Handle invalid username or one that isn't in the _inFlight list. - if (_inFlightDomainUserIdentityRequests.contains(username)) { // Success! Verified user. _verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username)); _inFlightDomainUserIdentityRequests.remove(username); } else { - // Unexpected response. - // ####### TODO + // Failure. + qDebug() << "Unexpected username in response for user details -" << username; } + // ####### TODO: Handle roles if available. [plugin] + } else { // Failure. + + // ####### TODO: Error fields to report. [plugin] + qDebug() << "Error in response for user details -" << rootObject["error"].toString(); + // ####### TODO: Is this the best way to handle _inFlightDomainUserIdentityRequests? // If there's a brief network glitch will it recover? // Perhaps clear on a timer? Cancel timer upon subsequent successful responses? diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 263d5b853d..0e1da5d6ee 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -155,7 +155,7 @@ private: void requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken); typedef QHash> DomainUserIdentities; // > - DomainUserIdentities _inFlightDomainUserIdentityRequests; // Keep track of domain user identity requests in progress. + DomainUserIdentities _inFlightDomainUserIdentityRequests; // Domain user identity requests currently in progress. DomainUserIdentities _verifiedDomainUserIdentities; // Verified domain users. void getDomainGroupMemberships(const QString& domainUserName); diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 7fc02893eb..5cf707e732 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -60,7 +60,12 @@ void DomainAccountManager::setAuthURL(const QUrl& authURL) { qCDebug(networking) << "AccountManager URL for authenticated requests has been changed to" << qPrintable(_authURL.toString()); - // ####### TODO: See AccountManager::setAuthURL(). + _access_token = ""; + _refresh_token = ""; + + // ####### TODO: Restore and refresh OAuth2 tokens if have them for this domain. + + // ####### TODO: Handle "keep me logged in". } } @@ -84,9 +89,7 @@ void DomainAccountManager::requestAccessToken(const QString& username, const QSt formData.append("scope=" + DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE); // ####### TODO: Include state? - QUrl domainAuthURL = _authURL; - domainAuthURL.setPath("/token"); // ####### TODO: miniOrange-mandated URL. ####### TODO: Should this be included in the server settings value? - request.setUrl(domainAuthURL); + request.setUrl(_authURL); request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); @@ -96,40 +99,40 @@ void DomainAccountManager::requestAccessToken(const QString& username, const QSt } void DomainAccountManager::requestAccessTokenFinished() { + QNetworkReply* requestReply = reinterpret_cast(sender()); QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); const QJsonObject& rootObject = jsonResponse.object(); - // ####### TODO: Test HTTP response codes rather than object contains "error". - // #### reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200 - if (!rootObject.contains("error")) { - // ####### TODO: Process response scope? - // ####### TODO: Process response state? + auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (200 <= httpStatus && httpStatus < 300) { + // ####### TODO: Check that token type == "Bearer"? + // ####### TODO: Process response state? + // ####### TODO: Process response scope? - if (!rootObject.contains("access_token") - // ####### TODO: Does WordPRess plugin provide "expires_in"? - // If so, handle here, or is it just the domain server that needs to use it? - //|| !rootObject.contains("expires_in") - || !rootObject.contains("token_type")) { + if (rootObject.contains("access_token")) { + // Success. - qCDebug(networking) << "Received a response for password grant that is missing one or more expected values."; - emit loginFailed(); - - } else { - - // clear the path from the response URL so we have the right root URL for this access token QUrl rootURL = requestReply->url(); rootURL.setPath(""); - qCDebug(networking) << "Storing a domain account with access-token for" << qPrintable(rootURL.toString()); - setAccessTokenFromJSON(rootObject); + setTokensFromJSON(rootObject, rootURL); - emit loginComplete(rootURL); + emit loginComplete(); + + } else { + // Failure. + qCDebug(networking) << "Received a response for password grant that is missing one or more expected values."; + emit loginFailed(); } + } else { - qCDebug(networking) << "Error in response for password grant -" << rootObject["error_description"].toString(); + // Failure. + + // ####### TODO: Error object fields to report. [plugin] + qCDebug(networking) << "Error in response for password grant -" << rootObject["error"].toString(); emit loginFailed(); } } @@ -171,13 +174,14 @@ bool DomainAccountManager::hasValidAccessToken() { } -void DomainAccountManager::setAccessTokenFromJSON(const QJsonObject& jsonObject) { +void DomainAccountManager::setTokensFromJSON(const QJsonObject& jsonObject, const QUrl& url) { _access_token = jsonObject["access_token"].toString(); _refresh_token = jsonObject["refresh_token"].toString(); // ####### TODO: Enable and use these. // ####### TODO: Protect these per AccountManager? /* + qCDebug(networking) << "Storing a domain account with access-token for" << qPrintable(url.toString()); domainAccessToken.set(jsonObject["access_token"].toString()); domainAccessRefreshToken.set(jsonObject["refresh_token"].toString()); domainAccessTokenExpiresIn.set(QDateTime::currentMSecsSinceEpoch() + (jsonObject["expires_in"].toDouble() * 1000)); diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index 08b625a246..6578963bf8 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -37,7 +37,7 @@ public slots: void requestAccessTokenFinished(); signals: void authRequired(); - void loginComplete(const QUrl& authURL); + void loginComplete(); void loginFailed(); void logoutComplete(); void newTokens(); @@ -47,7 +47,7 @@ private slots: private: bool hasValidAccessToken(); bool accessTokenIsExpired(); - void setAccessTokenFromJSON(const QJsonObject&); + void setTokensFromJSON(const QJsonObject&, const QUrl& url); void sendInterfaceAccessTokenToServer(); QUrl _authURL; From 26e6ce07d7068973d192300e9e8b5f74407fcc44 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 3 Aug 2020 11:26:11 +1200 Subject: [PATCH 14/22] Further networking tidying --- domain-server/src/DomainGatekeeper.cpp | 17 +++++------ .../networking/src/DomainAccountManager.cpp | 29 +++++++------------ 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 16c7383bf9..25f38d6ebd 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -1101,11 +1101,11 @@ void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply* requestReply void DomainGatekeeper::getDomainGroupMemberships(const QString& domainUserName) { - // ####### TODO: Get user's domain group memberships (WordPress roles) from domain. + // ####### TODO: Get user's domain group memberships (WordPress roles) from domain. [plugin groups] // This may be able to be provided at the same time as the "authenticate user" call to the domain API, in which case // a copy of some of the following code can be made there. However, this code is still needed for refreshing groups. - // ####### TODO: Check how often this method and the WordPress API is called. + // ####### TODO: Check how often this method and the WordPress API is called. [plugin groups] QStringList wordpressGroupsForUser; wordpressGroupsForUser << "silVER" << "gold" << "coal"; @@ -1161,7 +1161,7 @@ void DomainGatekeeper::getDomainOwnerFriendsListErrorCallback(QNetworkReply* req qDebug() << "getDomainOwnerFriendsList api call failed:" << requestReply->error(); } -// ####### TODO: Domain equivalent or addition +// ####### TODO: Domain equivalent or addition [plugin groups] void DomainGatekeeper::refreshGroupsCache() { // if agents are connected to this domain, refresh our cached information about groups and memberships in such. getDomainOwnerFriendsList(); @@ -1251,7 +1251,7 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& const QString WORDPRESS_USER_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; QUrl domainUserURL = apiBase + WORDPRESS_USER_ROUTE; - // ####### TODO: Append a random key to check in response? + // ####### TODO: Append a random key to check in response? [security] QNetworkRequest request; @@ -1265,8 +1265,6 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - // ####### TODO: Handle invalid URL (e.g., set timeout or similar). - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkReply* requestReply = networkAccessManager.post(request, formData); connect(requestReply, &QNetworkReply::finished, this, &DomainGatekeeper::requestDomainUserFinished); @@ -1280,6 +1278,7 @@ void DomainGatekeeper::requestDomainUserFinished() { const QJsonObject& rootObject = jsonResponse.object(); auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (200 <= httpStatus && httpStatus < 300) { // Success. // ####### TODO: Verify Expected response. [plugin] @@ -1307,11 +1306,9 @@ void DomainGatekeeper::requestDomainUserFinished() { // Failure. // ####### TODO: Error fields to report. [plugin] - qDebug() << "Error in response for user details -" << rootObject["error"].toString(); + qDebug() << "Error in response for user details -" << httpStatus << requestReply->error() + << "-" << rootObject["error"].toString(); - // ####### TODO: Is this the best way to handle _inFlightDomainUserIdentityRequests? - // If there's a brief network glitch will it recover? - // Perhaps clear on a timer? Cancel timer upon subsequent successful responses? _inFlightDomainUserIdentityRequests.clear(); } } diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 5cf707e732..cb67d30d3e 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -11,31 +11,23 @@ #include "DomainAccountManager.h" -// ####### TODO: Check that all #includes are still needed. - -#include - #include -#include #include #include #include #include -#include -#include "DomainAccountManager.h" +#include + #include "NetworkingConstants.h" -#include "OAuthAccessToken.h" #include "NetworkLogging.h" -#include "NodeList.h" -#include "udt/PacketHeaders.h" #include "NetworkAccessManager.h" // FIXME: Generalize to other OAuth2 sources for domain login. const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; -const QString DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE = "foo bar"; // ####### TODO: WordPress plugin's required scope. -// ####### TODO: Should scope be configured in domain server settings? +const QString DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE = "foo bar"; // ####### TODO: WordPress plugin's required scope. [plugin] +// ####### TODO: Should scope be configured in domain server settings? [plugin] // ####### TODO: Add storing domain URL and check against it when retrieving values? // ####### TODO: Add storing _authURL and check against it when retrieving values? @@ -79,7 +71,7 @@ void DomainAccountManager::requestAccessToken(const QString& username, const QSt request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - // ####### TODO: WordPress plugin's authorization requirements. + // ####### TODO: WordPress plugin's authorization requirements. [plugin] request.setRawHeader(QByteArray("Authorization"), QByteArray("Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x")); QByteArray formData; @@ -87,7 +79,7 @@ void DomainAccountManager::requestAccessToken(const QString& username, const QSt formData.append("username=" + QUrl::toPercentEncoding(username) + "&"); formData.append("password=" + QUrl::toPercentEncoding(password) + "&"); formData.append("scope=" + DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE); - // ####### TODO: Include state? + // ####### TODO: Include state? [plugin] request.setUrl(_authURL); @@ -108,10 +100,10 @@ void DomainAccountManager::requestAccessTokenFinished() { auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (200 <= httpStatus && httpStatus < 300) { - // ####### TODO: Check that token type == "Bearer"? - // ####### TODO: Process response state? - // ####### TODO: Process response scope? + // ####### TODO: Process response state? [plugin] + // ####### TODO: Process response scope? [plugin] + // ####### TODO: Which method are the tokens provided in? [plugin] if (rootObject.contains("access_token")) { // Success. @@ -132,7 +124,8 @@ void DomainAccountManager::requestAccessTokenFinished() { // Failure. // ####### TODO: Error object fields to report. [plugin] - qCDebug(networking) << "Error in response for password grant -" << rootObject["error"].toString(); + qCDebug(networking) << "Error in response for password grant -" << httpStatus << requestReply->error() + << "_" << rootObject["error"].toString(); emit loginFailed(); } } From 0b42ef57489579f05cbcef4a6c2aed6c9ac6f7fc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 3 Aug 2020 13:21:20 +1200 Subject: [PATCH 15/22] Regularize domain username case --- domain-server/src/DomainGatekeeper.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 25f38d6ebd..4b0eaab662 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -109,6 +109,7 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetBytesLeftToRead() > 0) { // Read domain username from packet. packetStream >> domainUsername; + domainUsername = domainUsername.toLower(); // Domain usernames are case-insensitive; internally lower-case. if (message->getBytesLeftToRead() > 0) { // Read domain tokens from packet. @@ -528,8 +529,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect } else if (verifyDomainUserIdentity(domainUsername, domainAccessToken, domainRefreshToken, nodeConnection.senderSockAddr)) { // User's domain identity is confirmed. - getDomainGroupMemberships(domainUsername); - verifiedDomainUsername = domainUsername.toLower(); + verifiedDomainUsername = domainUsername; + getDomainGroupMemberships(verifiedDomainUsername); } else { // User's domain identity didn't check out. From 257eadc99f84b97fe6a6cc4cfc1db0b0011cdec1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 3 Aug 2020 16:12:13 +1200 Subject: [PATCH 16/22] "Unable connect to this domain" message box after login if can't connect --- domain-server/src/DomainGatekeeper.cpp | 4 ++-- interface/src/ConnectionMonitor.cpp | 6 ++++++ libraries/networking/src/AccountManager.cpp | 4 +++- libraries/networking/src/DomainHandler.cpp | 4 +++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 4b0eaab662..8d0bc17f00 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -553,10 +553,10 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (domainAuthURLVariant.canConvert()) { domainAuthURL = domainAuthURLVariant.toString(); } - sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", + sendConnectionDeniedPacket("You lack the required domain permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain, domainAuthURL); } else { - sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", + sendConnectionDeniedPacket("You lack the required metaverse permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedMetaverse); } #ifdef WANT_DEBUG diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp index 33d9fddc1b..070015f05b 100644 --- a/interface/src/ConnectionMonitor.cpp +++ b/interface/src/ConnectionMonitor.cpp @@ -14,8 +14,10 @@ #include "Application.h" #include "ui/DialogsManager.h" +#include #include #include +#include #include #include @@ -34,6 +36,10 @@ void ConnectionMonitor::init() { connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ConnectionMonitor::stopTimer); connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &ConnectionMonitor::stopTimer); connect(this, &ConnectionMonitor::setRedirectErrorState, &domainHandler, &DomainHandler::setRedirectErrorState); + auto accountManager = DependencyManager::get(); + connect(accountManager.data(), &AccountManager::loginComplete, this, &ConnectionMonitor::startTimer); + auto domainAccountManager = DependencyManager::get(); + connect(domainAccountManager.data(), &DomainAccountManager::loginComplete, this, &ConnectionMonitor::startTimer); _timer.setSingleShot(true); if (!domainHandler.isConnected()) { diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 83c0fd28dd..5589defd80 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -508,7 +508,9 @@ bool AccountManager::checkAndSignalForAccessToken() { if (!hasToken) { // emit a signal so somebody can call back to us and request an access token given a username and password - emit authRequired(); + + // Dialog can be hidden immediately after showing if we've just teleported to the domain, unless the signal is delayed. + QTimer::singleShot(500, this, [this] { emit this->authRequired(); }); } return hasToken; diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 0a61036b96..7049f2aef6 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -550,7 +550,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer Date: Tue, 4 Aug 2020 09:15:05 +1200 Subject: [PATCH 17/22] Misc. tidying --- domain-server/src/DomainGatekeeper.cpp | 8 +------- domain-server/src/DomainServerSettingsManager.cpp | 2 +- interface/src/Application.cpp | 1 - 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 8d0bc17f00..9019a530d7 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -1218,7 +1218,7 @@ Node::LocalID DomainGatekeeper::findOrCreateLocalID(const QUuid& uuid) { bool DomainGatekeeper::domainHasLogin() { // The domain may have its own users and groups in a WordPress site. - // ####### TODO: Add checks of any further domain server settings used. + // ####### TODO: Add checks of any further domain server settings used. [plugin, groups] return _server->_settingsManager.valueForKeyPath(AUTHENTICATION_ENAABLED).toBool() && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_PATH).toString().isEmpty() && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString().isEmpty(); @@ -1226,12 +1226,6 @@ bool DomainGatekeeper::domainHasLogin() { void DomainGatekeeper::requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken) { - // ####### TODO: Move this further up the chain such that generates "invalid username or password" condition? - // Don't request identity for the standard psuedo-account-names. - if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) { - return; - } - if (_inFlightDomainUserIdentityRequests.contains(username)) { // Domain identify request for this username is already in progress. return; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 30ca15a51e..82ac0b265a 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -2186,7 +2186,7 @@ QList DomainServerSettingsManager::getBlacklistGroupIDs() { } QStringList DomainServerSettingsManager::getDomainGroupNames() { - // Names as configured in domain server; not necessarily mnetaverse groups. + // Names as configured in domain server; not necessarily metaverse groups. QSet result; foreach(NodePermissionsKey groupKey, _groupPermissions.keys()) { result += _groupPermissions[groupKey]->getID(); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 72e6af74ec..2c6702fbfc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -9444,7 +9444,6 @@ void Application::forceDisplayName(const QString& displayName) { getMyAvatar()->setDisplayName(displayName); } void Application::forceLoginWithTokens(const QString& tokens) { - // ####### TODO DependencyManager::get()->setAccessTokens(tokens); Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, true).set(true); } From bc56eb5ac75b5f677c01faa72e5f39f325f4f477 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Aug 2020 17:12:31 +1200 Subject: [PATCH 18/22] Update to work with WordPress plugin --- .../resources/describe-settings.json | 14 ++++---- domain-server/src/DomainGatekeeper.cpp | 36 +++++++++---------- .../networking/src/DomainAccountManager.cpp | 26 +++++--------- .../networking/src/DomainAccountManager.h | 2 ++ libraries/networking/src/DomainHandler.cpp | 4 ++- 5 files changed, 36 insertions(+), 46 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 5560fdba56..c901772296 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -77,13 +77,6 @@ "type": "checkbox", "advanced": true }, - { - "name": "domain_access_token", - "label": "Domain API Access Token", - "help": "This is the access token that your domain-server will use to verify users and their roles. This token must grant access to that permission set on your REST API server.", - "advanced": true, - "backup": false - }, { "name": "oauth2_url_path", "label": "Authentication URL", @@ -95,6 +88,13 @@ "label": "WordPress API URL Base", "help": "The URL base that the domain server will use to make WordPress API calls. Typically \"https://oursite.com/wp-json/\". However, if using non-pretty permalinks or otherwise get a 404 error then try \"https://oursite.com/?rest_route=/\".", "advanced": true + }, + { + "name": "plugin_client_id", + "label": "WordPress Plugin Client ID", + "help": "This is the client ID from the WordPress plugin configuration.", + "advanced": true, + "backup": false } ] }, diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 9019a530d7..ad637f3c17 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -453,6 +453,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo const QString AUTHENTICATION_ENAABLED = "authentication.enable_oauth2"; const QString AUTHENTICATION_OAUTH2_URL_PATH = "authentication.oauth2_url_path"; const QString AUTHENTICATION_WORDPRESS_URL_BASE = "authentication.wordpress_url_base"; +const QString AUTHENTICATION_PLUGIN_CLIENT_ID = "authentication.plugin_client_id"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; @@ -553,8 +554,15 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (domainAuthURLVariant.canConvert()) { domainAuthURL = domainAuthURLVariant.toString(); } + QString domainAuthClientID; + auto domainAuthClientIDVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_PLUGIN_CLIENT_ID); + if (domainAuthClientIDVariant.canConvert()) { + domainAuthClientID = domainAuthClientIDVariant.toString(); + } + sendConnectionDeniedPacket("You lack the required domain permissions to connect to this domain.", - nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain, domainAuthURL); + nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain, + domainAuthURL + "|" + domainAuthClientID); } else { sendConnectionDeniedPacket("You lack the required metaverse permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedMetaverse); @@ -1242,11 +1250,9 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& } // Get data pertaining to "me", the user who generated the access token. - // ####### TODO: Confirm API route and data w.r.t. OAuth2 plugin's capabilities. [plugin] - const QString WORDPRESS_USER_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; - QUrl domainUserURL = apiBase + WORDPRESS_USER_ROUTE; - - // ####### TODO: Append a random key to check in response? [security] + const QString WORDPRESS_USER_ROUTE = "wp/v2/users/me"; + const QString WORDPRESS_USER_QUERY = "_fields=username,roles"; + QUrl domainUserURL = apiBase + WORDPRESS_USER_ROUTE + (apiBase.contains("?") ? "&" : "?") + WORDPRESS_USER_QUERY; QNetworkRequest request; @@ -1275,34 +1281,24 @@ void DomainGatekeeper::requestDomainUserFinished() { auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (200 <= httpStatus && httpStatus < 300) { - // Success. - // ####### TODO: Verify Expected response. [plugin] - /* - { - id: 2, - username : 'apiuser', - roles : ['subscriber'] , - } - */ QString username = rootObject["username"].toString().toLower(); if (_inFlightDomainUserIdentityRequests.contains(username)) { // Success! Verified user. _verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username)); _inFlightDomainUserIdentityRequests.remove(username); + + // ####### TODO: Handle roles. + } else { // Failure. qDebug() << "Unexpected username in response for user details -" << username; } - // ####### TODO: Handle roles if available. [plugin] - } else { // Failure. - - // ####### TODO: Error fields to report. [plugin] qDebug() << "Error in response for user details -" << httpStatus << requestReply->error() - << "-" << rootObject["error"].toString(); + << "-" << rootObject["error"].toString() << rootObject["error_description"].toString(); _inFlightDomainUserIdentityRequests.clear(); } diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index cb67d30d3e..9910252317 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -26,8 +26,6 @@ // FIXME: Generalize to other OAuth2 sources for domain login. const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; -const QString DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE = "foo bar"; // ####### TODO: WordPress plugin's required scope. [plugin] -// ####### TODO: Should scope be configured in domain server settings? [plugin] // ####### TODO: Add storing domain URL and check against it when retrieving values? // ####### TODO: Add storing _authURL and check against it when retrieving values? @@ -71,15 +69,15 @@ void DomainAccountManager::requestAccessToken(const QString& username, const QSt request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - // ####### TODO: WordPress plugin's authorization requirements. [plugin] - request.setRawHeader(QByteArray("Authorization"), QByteArray("Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x")); + // miniOrange WordPress API Authentication plugin: + // - Requires "client_id" parameter. + // - Ignores "state" parameter. QByteArray formData; formData.append("grant_type=password&"); formData.append("username=" + QUrl::toPercentEncoding(username) + "&"); formData.append("password=" + QUrl::toPercentEncoding(password) + "&"); - formData.append("scope=" + DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE); - // ####### TODO: Include state? [plugin] + formData.append("client_id=" + _clientID); request.setUrl(_authURL); @@ -100,20 +98,13 @@ void DomainAccountManager::requestAccessTokenFinished() { auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (200 <= httpStatus && httpStatus < 300) { - // ####### TODO: Process response state? [plugin] - // ####### TODO: Process response scope? [plugin] - - // ####### TODO: Which method are the tokens provided in? [plugin] + // miniOrange plugin provides no scope. if (rootObject.contains("access_token")) { // Success. - QUrl rootURL = requestReply->url(); rootURL.setPath(""); - setTokensFromJSON(rootObject, rootURL); - emit loginComplete(); - } else { // Failure. qCDebug(networking) << "Received a response for password grant that is missing one or more expected values."; @@ -122,10 +113,8 @@ void DomainAccountManager::requestAccessTokenFinished() { } else { // Failure. - - // ####### TODO: Error object fields to report. [plugin] - qCDebug(networking) << "Error in response for password grant -" << httpStatus << requestReply->error() - << "_" << rootObject["error"].toString(); + qCDebug(networking) << "Error in response for password grant -" << httpStatus << requestReply->error() + << "-" << rootObject["error"].toString() << rootObject["error_description"].toString(); emit loginFailed(); } } @@ -173,6 +162,7 @@ void DomainAccountManager::setTokensFromJSON(const QJsonObject& jsonObject, cons // ####### TODO: Enable and use these. // ####### TODO: Protect these per AccountManager? + // ######: TODO: clientID needed? /* qCDebug(networking) << "Storing a domain account with access-token for" << qPrintable(url.toString()); domainAccessToken.set(jsonObject["access_token"].toString()); diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index 6578963bf8..213d40a8d8 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -24,6 +24,7 @@ public: DomainAccountManager(); void setAuthURL(const QUrl& authURL); + void setClientID(const QString& clientID) { _clientID = clientID; } QString getUsername() { return _username; } QString getAccessToken() { return _access_token; } @@ -51,6 +52,7 @@ private: void sendInterfaceAccessTokenToServer(); QUrl _authURL; + QString _clientID; QString _username; // ####### TODO: Store elsewhere? QString _access_token; // ####... "" QString _refresh_token; // ####... "" diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 7049f2aef6..c1a24748f4 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -589,7 +589,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer(); if (!extraInfo.isEmpty()) { - accountManager->setAuthURL(extraInfo); + auto extraInfoComponents = extraInfo.split("|"); + accountManager->setAuthURL(extraInfoComponents.value(0)); + accountManager->setClientID(extraInfoComponents.value(1)); } if (!_hasCheckedForDomainAccessToken) { From 3a43283e7b1d03545c85b7bf671f1fe9bc64a635 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Aug 2020 19:42:02 +1200 Subject: [PATCH 19/22] Provide domain name to domain login dialog --- .../resources/qml/LoginDialog/LinkAccountBody.qml | 1 + interface/src/ui/DialogsManager.cpp | 14 ++++++++++---- interface/src/ui/DialogsManager.h | 5 ++++- interface/src/ui/LoginDialog.cpp | 4 ++++ interface/src/ui/LoginDialog.h | 1 + libraries/networking/src/DomainAccountManager.cpp | 5 ++++- libraries/networking/src/DomainAccountManager.h | 3 ++- 7 files changed, 26 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 3d715d1a39..810a8a37dd 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -46,6 +46,7 @@ Item { readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() readonly property bool isLoggingInToDomain: loginDialog.getDomainLoginRequested() + readonly property string domainLoginDomain: loginDialog.getDomainLoginDomain() QtObject { id: d diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 848663967f..6e807c7b9f 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -110,8 +110,14 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { } } + +void DialogsManager::setDomainLogin(bool isDomainLogin, const QString& domain) { + _isDomainLogin = isDomainLogin; + _domainLoginDomain = domain; +} + void DialogsManager::toggleLoginDialog() { - _isDomainLogin = false; + setDomainLogin(false); LoginDialog::toggleAction(); } @@ -120,7 +126,7 @@ void DialogsManager::showLoginDialog() { // ####### TODO: May be called from script via DialogsManagerScriptingInterface. Need to handle the case that it's already // displayed and may be the domain login version. - _isDomainLogin = false; + setDomainLogin(false); LoginDialog::showWithSelection(); } @@ -129,8 +135,8 @@ void DialogsManager::hideLoginDialog() { } -void DialogsManager::showDomainLoginDialog() { - _isDomainLogin = true; +void DialogsManager::showDomainLoginDialog(const QString& domain) { + setDomainLogin(true, domain); LoginDialog::showWithSelection(); } diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 30127ced68..fa5c589fb4 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -42,6 +42,7 @@ public: void emitAddressBarShown(bool visible) { emit addressBarShown(visible); } void setAddressBarVisible(bool addressBarVisible); bool getIsDomainLogin() { return _isDomainLogin; } + QString getDomainLoginDomain() { return _domainLoginDomain; } public slots: void showAddressBar(); @@ -51,7 +52,7 @@ public slots: void toggleLoginDialog(); void showLoginDialog(); void hideLoginDialog(); - void showDomainLoginDialog(); + void showDomainLoginDialog(const QString& domain); void octreeStatsDetails(); void lodTools(); void hmdTools(bool showTools); @@ -86,7 +87,9 @@ private: bool _dialogCreatedWhileShown { false }; bool _addressBarVisible { false }; + void setDomainLogin(bool isDomainLogin, const QString& domain = ""); bool _isDomainLogin { false }; + QString _domainLoginDomain; }; #endif // hifi_DialogsManager_h diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 3cc37bcadb..c7597c5658 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -427,3 +427,7 @@ void LoginDialog::signupFailed(QNetworkReply* reply) { bool LoginDialog::getDomainLoginRequested() const { return DependencyManager::get()->getIsDomainLogin(); } + +QString LoginDialog::getDomainLoginDomain() const { + return DependencyManager::get()->getDomainLoginDomain(); +} diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 310a5db255..9f4af5debb 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -85,6 +85,7 @@ protected slots: Q_INVOKABLE bool getLoginDialogPoppedUp() const; Q_INVOKABLE bool getDomainLoginRequested() const; + Q_INVOKABLE QString getDomainLoginDomain() const; }; diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 9910252317..6bb868df61 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -184,7 +184,10 @@ bool DomainAccountManager::checkAndSignalForAccessToken() { // Emit a signal so somebody can call back to us and request an access token given a user name and password. // Dialog can be hidden immediately after showing if we've just teleported to the domain, unless the signal is delayed. - QTimer::singleShot(500, this, [this] { emit this->authRequired(); }); + auto domain = _authURL.host(); + QTimer::singleShot(500, this, [this, domain] { + emit this->authRequired(domain); + }); } return hasToken; diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index 213d40a8d8..0388ba1f5a 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -36,8 +36,9 @@ public slots: void requestAccessToken(const QString& username, const QString& password); void requestAccessTokenFinished(); + signals: - void authRequired(); + void authRequired(const QString& domain); void loginComplete(); void loginFailed(); void logoutComplete(); From 60048162c086911dce0f09e267d63b61e85f44ca Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Aug 2020 21:00:09 +1200 Subject: [PATCH 20/22] Use WordPress user roles as domain groups --- domain-server/src/DomainGatekeeper.cpp | 25 ++++++++----------------- domain-server/src/DomainGatekeeper.h | 1 - 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index ad637f3c17..90a6ee9f5f 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -531,7 +531,6 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect nodeConnection.senderSockAddr)) { // User's domain identity is confirmed. verifiedDomainUsername = domainUsername; - getDomainGroupMemberships(verifiedDomainUsername); } else { // User's domain identity didn't check out. @@ -1108,20 +1107,6 @@ void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply* requestReply } -void DomainGatekeeper::getDomainGroupMemberships(const QString& domainUserName) { - - // ####### TODO: Get user's domain group memberships (WordPress roles) from domain. [plugin groups] - // This may be able to be provided at the same time as the "authenticate user" call to the domain API, in which case - // a copy of some of the following code can be made there. However, this code is still needed for refreshing groups. - - // ####### TODO: Check how often this method and the WordPress API is called. [plugin groups] - - QStringList wordpressGroupsForUser; - wordpressGroupsForUser << "silVER" << "gold" << "coal"; - _domainGroupMemberships[domainUserName] = wordpressGroupsForUser; -} - - void DomainGatekeeper::getDomainOwnerFriendsList() { JSONCallbackParameters callbackParams; callbackParams.callbackReceiver = this; @@ -1282,13 +1267,19 @@ void DomainGatekeeper::requestDomainUserFinished() { if (200 <= httpStatus && httpStatus < 300) { - QString username = rootObject["username"].toString().toLower(); + QString username = rootObject.value("username").toString().toLower(); if (_inFlightDomainUserIdentityRequests.contains(username)) { // Success! Verified user. _verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username)); _inFlightDomainUserIdentityRequests.remove(username); - // ####### TODO: Handle roles. + // User user's WordPress roles as domain groups. + QStringList domainUserGroups; + auto userRoles = rootObject.value("roles").toArray(); + foreach (auto role, userRoles) { + domainUserGroups.append(role.toString()); + } + _domainGroupMemberships[username] = domainUserGroups; } else { // Failure. diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 0e1da5d6ee..cf41786e4a 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -158,7 +158,6 @@ private: DomainUserIdentities _inFlightDomainUserIdentityRequests; // Domain user identity requests currently in progress. DomainUserIdentities _verifiedDomainUserIdentities; // Verified domain users. - void getDomainGroupMemberships(const QString& domainUserName); QHash _domainGroupMemberships; // }; From 6f9b47c07d307a34403fa611098eb6be8e48e106 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Aug 2020 21:31:13 +1200 Subject: [PATCH 21/22] Distinguish domain groups with a leading "@" --- domain-server/src/DomainGatekeeper.cpp | 7 ++++--- domain-server/src/DomainGatekeeper.h | 2 ++ domain-server/src/DomainServerSettingsManager.cpp | 4 ++++ domain-server/src/DomainServerSettingsManager.h | 8 ++++---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 90a6ee9f5f..5b0c526d9f 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -181,7 +181,7 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin auto userGroups = _domainGroupMemberships[verifiedDomainUserName]; foreach (QString userGroup, userGroups) { // Domain groups may be specified as comma- and/or space-separated lists of group names. - // For example, "silver gold, platinum". + // For example, "@silver @Gold, @platinum". auto domainGroups = _server->_settingsManager.getDomainGroupNames() .filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$", QRegularExpression::CaseInsensitiveOption)); @@ -302,7 +302,7 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin auto userGroups = _domainGroupMemberships[verifiedDomainUserName]; foreach(QString userGroup, userGroups) { // Domain groups may be specified as comma- and/or space-separated lists of group names. - // For example, "silver gold, platinum". + // For example, "@silver @Gold, @platinum". auto domainGroups = _server->_settingsManager.getDomainBlacklistGroupNames() .filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$", QRegularExpression::CaseInsensitiveOption)); @@ -1277,7 +1277,8 @@ void DomainGatekeeper::requestDomainUserFinished() { QStringList domainUserGroups; auto userRoles = rootObject.value("roles").toArray(); foreach (auto role, userRoles) { - domainUserGroups.append(role.toString()); + // Distinguish domain groups from metaverse groups by a leading special character. + domainUserGroups.append(DOMAIN_GROUP_CHAR + role.toString().toLower()); } _domainGroupMemberships[username] = domainUserGroups; diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index cf41786e4a..cb42baa7e3 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -30,6 +30,8 @@ #include "NodeConnectionData.h" #include "PendingAssignedNodeData.h" +const QString DOMAIN_GROUP_CHAR = "@"; + class DomainServer; class DomainGatekeeper : public QObject { diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 82ac0b265a..c03f26f0a1 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -1966,6 +1966,10 @@ void DomainServerSettingsManager::apiRefreshGroupInformation() { QStringList groupNames = getAllKnownGroupNames(); foreach (QString groupName, groupNames) { QString lowerGroupName = groupName.toLower(); + if (lowerGroupName.contains(DOMAIN_GROUP_CHAR)) { + // Ignore domain groups. + return; + } if (_groupIDs.contains(lowerGroupName)) { // we already know about this one. recall setGroupID in case the group has been // added to another section (the same group is found in both groups and blacklists). diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 8c18c22b32..73aef07835 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -19,11 +19,11 @@ #include #include - -#include -#include "NodePermissions.h" - #include +#include + +#include "DomainGatekeeper.h" +#include "NodePermissions.h" const QString SETTINGS_PATHS_KEY = "paths"; From dfef1fb5bbcb1c5fcc31d3f520f6c16ced524e08 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Aug 2020 22:03:08 +1200 Subject: [PATCH 22/22] Remove unused domain server setting --- domain-server/resources/describe-settings.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index c901772296..8e57a9b683 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -69,14 +69,6 @@ "type": "checkbox", "advanced": true }, - { - "name": "require_oauth2", - "label": "Require OAuth2 Authentication", - "help": "For any users not explicitly authorized in these settings, notify the Interface to authenticate through this method.", - "default": false, - "type": "checkbox", - "advanced": true - }, { "name": "oauth2_url_path", "label": "Authentication URL",