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()); } }