Verify user at domain server

This commit is contained in:
David Rowe 2020-08-02 16:17:04 +12:00
parent 15c6baceb8
commit 8cdd76a42e
5 changed files with 157 additions and 39 deletions

View file

@ -90,12 +90,13 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
SharedNodePointer node;
QString username;
QString domainUsername;
if (pendingAssignment != _pendingAssignedNodes.end()) {
node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second);
} 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(QSharedPointer<ReceivedMessag
if (message->getBytesLeftToRead() > 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<LimitedNodeList>();
@ -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<QString, QString>(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<quint16> 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<QString, QString>(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<QNetworkReply*>(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();
}
}

View file

@ -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<QString, QStringList> _domainGroupMemberships; // <domainUserName, [domainGroupName]>
// Local ID management.
void initLocalIDManagement();
using UUIDToLocalID = std::unordered_map<QUuid, Node::LocalID> ;
using LocalIDs = std::unordered_set<Node::LocalID>;
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<QString, QPair<QString, QString>> DomainUserIdentities; // <domainUserName, <access_token, refresh_token>>
DomainUserIdentities _inFlightDomainUserIdentityRequests; // Keep track of domain user identity requests in progress.
DomainUserIdentities _verifiedDomainUserIdentities; // Verified domain users.
void getDomainGroupMemberships(const QString& domainUserName);
QHash<QString, QStringList> _domainGroupMemberships; // <domainUserName, [domainGroupName]>
};

View file

@ -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"?

View file

@ -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);

View file

@ -499,7 +499,8 @@ void NodeList::sendDomainServerCheckIn() {
auto domainAccountManager = DependencyManager::get<DomainAccountManager>();
if (!domainAccountManager->getUsername().isEmpty()) {
packetStream << domainAccountManager->getUsername();
packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken());
if (!domainAccountManager->getAccessToken().isEmpty()) {
packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken());
}
}