Merge branch 'oauth2-wordpress' into feature/oauth2-kalila

This commit is contained in:
Kasen IO 2020-08-04 14:17:48 -04:00
commit f066eccba0
20 changed files with 437 additions and 190 deletions

View file

@ -70,25 +70,23 @@
"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",
"name": "oauth2_url_path",
"label": "Authentication URL",
"help": "The URL that the Interface will use to login via OAuth2.",
"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.",
"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 \"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
},
{
"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
}
]
},

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;
QByteArray domainUsernameSignature;
QString domainUsername;
QStringList domainTokens;
if (message->getBytesLeftToRead() > 0) {
// read username from packet
@ -108,16 +109,21 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
if (message->getBytesLeftToRead() > 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 signature from packet.
packetStream >> domainUsernameSignature;
// Read domain tokens from packet.
QString domainTokensString;
packetStream >> domainTokensString;
domainTokens = domainTokensString.split(":");
}
}
}
}
node = processAgentConnectRequest(nodeConnection, username, usernameSignature, domainUsername, domainUsernameSignature);
node = processAgentConnectRequest(nodeConnection, username, usernameSignature,
domainUsername, domainTokens.value(0), domainTokens.value(1));
}
if (node) {
@ -156,10 +162,8 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
}
NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername,
QString verifiedDomainUserName,
QStringList verifiedDomainUserGroups,
const QHostAddress& senderAddress, const QString& hardwareAddress,
const QUuid& machineFingerprint) {
QString verifiedDomainUserName, const QHostAddress& senderAddress,
const QString& hardwareAddress, const QUuid& machineFingerprint) {
NodePermissions userPerms;
userPerms.setAll(false);
@ -171,21 +175,25 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
#endif
}
// If this user is a known member of an externally-hosted group, give them the implied permissions.
// If this user is a known member of a domain group, give them the implied permissions.
// Do before processing verifiedUsername in case user is logged into the metaverse and is a member of a blacklist group.
if (!verifiedDomainUserName.isEmpty() && !verifiedDomainUserGroups.isEmpty()) {
foreach (QString group, verifiedDomainUserGroups) {
if (_server->_settingsManager.getAllKnownGroupNames().contains(group)) {
userPerms |= _server->_settingsManager.getPermissionsForGroup(group, QUuid());
//#ifdef WANT_DEBUG
qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << group << "so:" << userPerms;
//#endif
if (!verifiedDomainUserName.isEmpty()) {
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".
auto domainGroups = _server->_settingsManager.getDomainGroupNames()
.filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$",
QRegularExpression::CaseInsensitiveOption));
foreach(QString domainGroup, domainGroups) {
userPerms |= _server->_settingsManager.getPermissionsForGroup(domainGroup, QUuid()); // No rank for domain groups.
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << domainGroup
<< "so:" << userPerms;
#endif
}
}
userPerms.setVerifiedDomainUserName(verifiedDomainUserName);
userPerms.setVerifiedDomainUserGroups(verifiedDomainUserGroups);
}
if (verifiedUsername.isEmpty()) {
@ -289,6 +297,26 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
userPerms.setVerifiedUserName(verifiedUsername);
}
// If this user is a known member of an domain group that is blacklisted, remove the implied permissions.
if (!verifiedDomainUserName.isEmpty()) {
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".
auto domainGroups = _server->_settingsManager.getDomainBlacklistGroupNames()
.filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$",
QRegularExpression::CaseInsensitiveOption));
foreach(QString domainGroup, domainGroups) {
userPerms &= ~_server->_settingsManager.getForbiddensForGroup(domainGroup, QUuid());
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: domain user is in blacklist group:" << domainGroup << "so:" << userPerms;
#endif
}
}
userPerms.setVerifiedDomainUserName(verifiedDomainUserName);
}
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: final:" << userPerms;
#endif
@ -309,7 +337,6 @@ void DomainGatekeeper::updateNodePermissions() {
// authentication and verifiedUsername is only set once they user's key has been confirmed.
QString verifiedUsername = node->getPermissions().getVerifiedUserName();
QString verifiedDomainUserName = node->getPermissions().getVerifiedDomainUserName();
QStringList verifiedDomainUserGroups = node->getPermissions().getVerifiedDomainUserGroups();
NodePermissions userPerms(NodePermissionsKey(verifiedUsername, 0));
if (node->getPermissions().isAssignment) {
@ -345,8 +372,7 @@ void DomainGatekeeper::updateNodePermissions() {
}
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUserName,
verifiedDomainUserGroups, connectingAddr.getAddress(),
hardwareAddress, machineFingerprint);
connectingAddr.getAddress(), hardwareAddress, machineFingerprint);
}
node->setPermissions(userPerms);
@ -424,6 +450,10 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
return newNode;
}
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";
@ -431,7 +461,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
const QString& username,
const QByteArray& usernameSignature,
const QString& domainUsername,
const QByteArray& domainUsernameSignature) {
const QString& domainAccessToken,
const QString& domainRefreshToken) {
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
@ -481,45 +512,58 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
QString verifiedDomainUsername;
QStringList verifiedDomainUserGroups;
if (domainHasLogin() && !domainUsername.isEmpty()) {
if (domainUsernameSignature.isEmpty()) {
if (domainAccessToken.isEmpty()) {
// User is attempting to prove their domain identity.
// ####### TODO: OAuth2 corollary of metaverse code, above.
return SharedNodePointer();
} else if (verifyDomainUserSignature(domainUsername, domainUsernameSignature, nodeConnection.senderSockAddr)) {
// User's domain identity is confirmed.
// ####### TODO: Get user's domain group memberships (WordPress roles) from domain.
// This may already be provided at the same time as the "verify" call to the domain API.
// If it isn't, need to initiate getting them then handle their receipt along the lines of the
// metaverse code, above.
verifiedDomainUserGroups = QString("test-group").toLower().split(" ");
verifiedDomainUsername = domainUsername.toLower();
} else {
// User's identity didn't check out.
// ####### TODO: OAuth2 corollary of metaverse code, above.
#ifdef WANT_DEBUG
qDebug() << "stalling login because signature verification failed:" << username;
qDebug() << "Stalling login because we have no domain OAuth2 tokens:" << domainUsername;
#endif
return SharedNodePointer();
} else if (needToVerifyDomainUserIdentity(domainUsername, domainAccessToken, domainRefreshToken)) {
// User's domain identity needs to be confirmed.
requestDomainUser(domainUsername, domainAccessToken, domainRefreshToken);
#ifdef WANT_DEBUG
qDebug() << "Stalling login because we haven't authenticated user yet:" << domainUsername;
#endif
} else if (verifyDomainUserIdentity(domainUsername, domainAccessToken, domainRefreshToken,
nodeConnection.senderSockAddr)) {
// User's domain identity is confirmed.
verifiedDomainUsername = domainUsername;
} else {
// User's domain identity didn't check out.
#ifdef WANT_DEBUG
qDebug() << "Stalling login because domain user verification failed:" << domainUsername;
#endif
return SharedNodePointer();
}
}
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUsername, verifiedDomainUserGroups,
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUsername,
nodeConnection.senderSockAddr.getAddress(), nodeConnection.hardwareAddress,
nodeConnection.machineFingerprint);
if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) {
if (domainHasLogin()) {
sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.",
nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain);
QString domainAuthURL;
auto domainAuthURLVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_PATH);
if (domainAuthURLVariant.canConvert<QString>()) {
domainAuthURL = domainAuthURLVariant.toString();
}
QString domainAuthClientID;
auto domainAuthClientIDVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_PLUGIN_CLIENT_ID);
if (domainAuthClientIDVariant.canConvert<QString>()) {
domainAuthClientID = domainAuthClientIDVariant.toString();
}
sendConnectionDeniedPacket("You lack the required domain permissions to connect to this domain.",
nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain,
domainAuthURL + "|" + domainAuthClientID);
} 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
@ -717,17 +761,21 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
return false;
}
bool DomainGatekeeper::verifyDomainUserSignature(const QString& domainUsername,
const QByteArray& domainUsernameSignature,
const HifiSockAddr& senderSockAddr) {
// ####### TODO: Verify via domain OAuth2.
bool success = true;
if (success) {
bool DomainGatekeeper::needToVerifyDomainUserIdentity(const QString& username, const QString& accessToken,
const QString& refreshToken) {
return !_verifiedDomainUserIdentities.contains(username)
|| _verifiedDomainUserIdentities.value(username) != QPair<QString, QString>(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<QString, QString>(accessToken, refreshToken)) {
return true;
}
sendConnectionDeniedPacket("Error decrypting domain username signature.", senderSockAddr,
sendConnectionDeniedPacket("Error verifying domain user.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::LoginErrorDomain);
return false;
}
@ -1004,7 +1052,6 @@ void DomainGatekeeper::getGroupMemberships(const QString& username) {
AccountManagerAuth::Required,
QNetworkAccessManager::PostOperation, callbackParams,
QJsonDocument(json).toJson());
}
QString extractUsernameFromGroupMembershipsReply(QNetworkReply* requestReply) {
@ -1059,6 +1106,7 @@ void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply* requestReply
_inFlightGroupMembershipsRequests.remove(extractUsernameFromGroupMembershipsReply(requestReply));
}
void DomainGatekeeper::getDomainOwnerFriendsList() {
JSONCallbackParameters callbackParams;
callbackParams.callbackReceiver = this;
@ -1107,6 +1155,7 @@ void DomainGatekeeper::getDomainOwnerFriendsListErrorCallback(QNetworkReply* req
qDebug() << "getDomainOwnerFriendsList api call failed:" << requestReply->error();
}
// ####### 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();
@ -1131,17 +1180,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: 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
return true;
}
void DomainGatekeeper::initLocalIDManagement() {
std::uniform_int_distribution<quint16> sixteenBitRand;
std::random_device randomDevice;
@ -1169,3 +1207,91 @@ Node::LocalID DomainGatekeeper::findOrCreateLocalID(const QUuid& uuid) {
_localIDs.insert(newLocalID);
return newLocalID;
}
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. [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();
}
void DomainGatekeeper::requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken) {
if (_inFlightDomainUserIdentityRequests.contains(username)) {
// Domain identify request for this username is already in progress.
return;
}
_inFlightDomainUserIdentityRequests.insert(username, QPair<QString, QString>(accessToken, refreshToken));
if (_verifiedDomainUserIdentities.contains(username)) {
_verifiedDomainUserIdentities.remove(username);
}
QString apiBase = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString();
if (!apiBase.endsWith("/")) {
apiBase += "/";
}
// Get data pertaining to "me", the user who generated the access token.
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;
request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setRawHeader(QByteArray("Authorization"), QString("Bearer " + accessToken).toUtf8());
QByteArray formData; // No data to send.
request.setUrl(domainUserURL);
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
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());
QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll());
const QJsonObject& rootObject = jsonResponse.object();
auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (200 <= httpStatus && httpStatus < 300) {
QString username = rootObject.value("username").toString().toLower();
if (_inFlightDomainUserIdentityRequests.contains(username)) {
// Success! Verified user.
_verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username));
_inFlightDomainUserIdentityRequests.remove(username);
// User user's WordPress roles as domain groups.
QStringList domainUserGroups;
auto userRoles = rootObject.value("roles").toArray();
foreach (auto role, userRoles) {
// Distinguish domain groups from metaverse groups by a leading special character.
domainUserGroups.append(DOMAIN_GROUP_CHAR + role.toString().toLower());
}
_domainGroupMemberships[username] = domainUserGroups;
} else {
// Failure.
qDebug() << "Unexpected username in response for user details -" << username;
}
} else {
// Failure.
qDebug() << "Error in response for user details -" << httpStatus << requestReply->error()
<< "-" << rootObject["error"].toString() << rootObject["error_description"].toString();
_inFlightDomainUserIdentityRequests.clear();
}
}

View file

@ -30,6 +30,8 @@
#include "NodeConnectionData.h"
#include "PendingAssignedNodeData.h"
const QString DOMAIN_GROUP_CHAR = "@";
class DomainServer;
class DomainGatekeeper : public QObject {
@ -72,6 +74,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,14 +85,16 @@ private:
const QString& username,
const QByteArray& usernameSignature,
const QString& domainUsername,
const QByteArray& domainUsernameSignature);
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 QByteArray& domainUsernameSignature,
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();
@ -128,24 +136,31 @@ private:
QSet<QString> _inFlightGroupMembershipsRequests; // keep track of which we've already asked for
NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, QString verifiedDomainUsername,
QStringList verifiedDomainUserGroups, const QHostAddress& senderAddress,
const QString& hardwareAddress, const QUuid& machineFingerprint);
const QHostAddress& senderAddress, const QString& hardwareAddress,
const QUuid& machineFingerprint);
void getGroupMemberships(const QString& username);
// void getIsGroupMember(const QString& username, const QUuid groupID);
void getDomainOwnerFriendsList();
bool domainHasLogin();
// 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; // Domain user identity requests currently in progress.
DomainUserIdentities _verifiedDomainUserIdentities; // Verified domain users.
QHash<QString, QStringList> _domainGroupMemberships; // <domainUserName, [domainGroupName]>
};

View file

@ -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).
@ -2185,6 +2189,24 @@ QList<QUuid> DomainServerSettingsManager::getBlacklistGroupIDs() {
return result.toList();
}
QStringList DomainServerSettingsManager::getDomainGroupNames() {
// Names as configured in domain server; not necessarily metaverse groups.
QSet<QString> result;
foreach(NodePermissionsKey groupKey, _groupPermissions.keys()) {
result += _groupPermissions[groupKey]->getID();
}
return result.toList();
}
QStringList DomainServerSettingsManager::getDomainBlacklistGroupNames() {
// Names as configured in domain server; not necessarily mnetaverse groups.
QSet<QString> result;
foreach (NodePermissionsKey groupKey, _groupForbiddens.keys()) {
result += _groupForbiddens[groupKey]->getID();
}
return result.toList();
}
void DomainServerSettingsManager::debugDumpGroupsState() {
qDebug() << "--------- GROUPS ---------";

View file

@ -19,11 +19,11 @@
#include <HifiConfigVariantMap.h>
#include <HTTPManager.h>
#include <ReceivedMessage.h>
#include "NodePermissions.h"
#include <Node.h>
#include <ReceivedMessage.h>
#include "DomainGatekeeper.h"
#include "NodePermissions.h"
const QString SETTINGS_PATHS_KEY = "paths";
@ -105,6 +105,9 @@ public:
QList<QUuid> getGroupIDs();
QList<QUuid> getBlacklistGroupIDs();
QStringList getDomainGroupNames();
QStringList getDomainBlacklistGroupNames();
// these are used to locally cache the result of calling "api/v1/groups/.../is_member/..." on metaverse's api
void clearGroupMemberships(const QString& name) { _groupMembership[name.toLower()].clear(); }
void recordGroupMembership(const QString& name, const QUuid groupID, QUuid rankID);

View file

@ -47,7 +47,7 @@ Item {
readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp()
// If not logging into domain, then we must be logging into the metaverse...
readonly property bool isLoggingInToDomain: loginDialog.getDomainLoginRequested()
readonly property string domainAuthProvider: loginDialog.getDomainLoginAuthProvider()
readonly property string domainLoginDomain: loginDialog.getDomainLoginDomain()
QtObject {
id: d
@ -77,7 +77,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) {

View file

@ -9444,7 +9444,6 @@ void Application::forceDisplayName(const QString& displayName) {
getMyAvatar()->setDisplayName(displayName);
}
void Application::forceLoginWithTokens(const QString& tokens) {
// ####### TODO
DependencyManager::get<AccountManager>()->setAccessTokens(tokens);
Setting::Handle<bool>(KEEP_ME_LOGGED_IN_SETTING_NAME, true).set(true);
}

View file

@ -14,8 +14,10 @@
#include "Application.h"
#include "ui/DialogsManager.h"
#include <AccountManager.h>
#include <DependencyManager.h>
#include <DomainHandler.h>
#include <DomainAccountManager.h>
#include <AddressManager.h>
#include <NodeList.h>
@ -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<AccountManager>();
connect(accountManager.data(), &AccountManager::loginComplete, this, &ConnectionMonitor::startTimer);
auto domainAccountManager = DependencyManager::get<DomainAccountManager>();
connect(domainAccountManager.data(), &DomainAccountManager::loginComplete, this, &ConnectionMonitor::startTimer);
_timer.setSingleShot(true);
if (!domainHandler.isConnected()) {

View file

@ -29,7 +29,6 @@
#include "OctreeStatsDialog.h"
#include "PreferencesDialog.h"
#include "UpdateDialog.h"
#include "DomainHandler.h"
#include "scripting/HMDScriptingInterface.h"
@ -111,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();
}
@ -121,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();
}
@ -130,20 +135,16 @@ void DialogsManager::hideLoginDialog() {
}
void DialogsManager::showDomainLoginDialog() {
const QJsonObject& settingsObject = DependencyManager::get<NodeList>()->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;
void DialogsManager::showDomainLoginDialog(const QString& domain) {
setDomainLogin(true, domain);
LoginDialog::showWithSelection();
}
// #######: TODO: Domain version of toggleLoginDialog()?
// #######: TODO: Domain version of hiadLoginDialog()?
void DialogsManager::showUpdateDialog() {
UpdateDialog::show();
}

View file

@ -42,7 +42,7 @@ public:
void emitAddressBarShown(bool visible) { emit addressBarShown(visible); }
void setAddressBarVisible(bool addressBarVisible);
bool getIsDomainLogin() { return _isDomainLogin; }
QString getDomainLoginAuthProvider() { return _domainLoginAuthProvider; }
QString getDomainLoginDomain() { return _domainLoginDomain; }
public slots:
void showAddressBar();
@ -52,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);
@ -87,8 +87,9 @@ private:
bool _dialogCreatedWhileShown { false };
bool _addressBarVisible { false };
void setDomainLogin(bool isDomainLogin, const QString& domain = "");
bool _isDomainLogin { false };
QString _domainLoginAuthProvider { "" };
QString _domainLoginDomain;
};
#endif // hifi_DialogsManager_h

View file

@ -143,9 +143,9 @@ void LoginDialog::login(const QString& username, const QString& password) const
DependencyManager::get<AccountManager>()->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<DomainAccountManager>()->requestAccessToken(username, password, domainAuthProvider);
void LoginDialog::loginDomain(const QString& username, const QString& password) const {
qDebug() << "Attempting to login" << username << "into a domain";
DependencyManager::get<DomainAccountManager>()->requestAccessToken(username, password);
}
void LoginDialog::loginThroughOculus() {
@ -428,6 +428,6 @@ bool LoginDialog::getDomainLoginRequested() const {
return DependencyManager::get<DialogsManager>()->getIsDomainLogin();
}
QString LoginDialog::getDomainLoginAuthProvider() const {
return DependencyManager::get<DialogsManager>()->getDomainLoginAuthProvider();
QString LoginDialog::getDomainLoginDomain() const {
return DependencyManager::get<DialogsManager>()->getDomainLoginDomain();
}

View file

@ -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,7 @@ protected slots:
Q_INVOKABLE bool getLoginDialogPoppedUp() const;
Q_INVOKABLE bool getDomainLoginRequested() const;
Q_INVOKABLE QString getDomainLoginAuthProvider() const;
Q_INVOKABLE QString getDomainLoginDomain() const;
};

View file

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

View file

@ -11,100 +11,124 @@
#include "DomainAccountManager.h"
#include <SettingHandle.h>
#include <QTimer>
#include <QtCore/QDateTime>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QHttpMultiPart>
#include "DomainAccountManager.h"
#include <SettingHandle.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";
// FIXME: Generalize to other OAuth2 sources for domain login.
const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false;
// ####### 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<QString> domainAccessToken {"private/domainAccessToken", "" };
Setting::Handle<QString> domainAccessRefreshToken {"private/domainAccessToken", "" };
Setting::Handle<int> domainAccessTokenExpiresIn {"private/domainAccessTokenExpiresIn", -1 };
Setting::Handle<QString> 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() {
DomainAccountManager::DomainAccountManager() :
_authURL(),
_username(),
_access_token(),
_refresh_token()
{
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;
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
qCDebug(networking) << "AccountManager URL for authenticated requests has been changed to" << qPrintable(_authURL.toString());
_access_token = "";
_refresh_token = "";
// ####### TODO: Restore and refresh OAuth2 tokens if have them for this domain.
// ####### TODO: Handle "keep me logged in".
}
}
void DomainAccountManager::requestAccessToken(const QString& username, const QString& password) {
_username = username;
_access_token = "";
_refresh_token = "";
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);
// 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("client_id=" + _clientID);
request.setUrl(_authURL);
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* requestReply = networkAccessManager.post(request, formData);
connect(requestReply, &QNetworkReply::finished, this, &DomainAccountManager::requestAccessTokenFinished);
}
void DomainAccountManager::requestAccessTokenFinished() {
QNetworkReply* requestReply = reinterpret_cast<QNetworkReply*>(sender());
QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll());
const QJsonObject& rootObject = jsonResponse.object();
if (!rootObject.contains("error")) {
// construct an OAuthAccessToken from the json object
auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (200 <= httpStatus && httpStatus < 300) {
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
// miniOrange plugin provides no scope.
if (rootObject.contains("access_token")) {
// Success.
QUrl rootURL = requestReply->url();
rootURL.setPath("");
qCDebug(networking) << "Storing a domain account with access-token for" << qPrintable(rootURL.toString());
setAccessTokenFromJSON(rootObject);
emit loginComplete(rootURL);
setTokensFromJSON(rootObject, 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.
qCDebug(networking) << "Error in response for password grant -" << httpStatus << requestReply->error()
<< "-" << rootObject["error"].toString() << rootObject["error_description"].toString();
emit loginFailed();
}
}
void DomainAccountManager::sendInterfaceAccessTokenToServer() {
// TODO: Send successful packet to the domain-server.
emit newTokens();
}
bool DomainAccountManager::accessTokenIsExpired() {
// ####### TODO: accessTokenIsExpired()
return true;
/*
return domainAccessTokenExpiresIn.get() != -1 && domainAccessTokenExpiresIn.get() <= QDateTime::currentMSecsSinceEpoch();
*/
}
@ -115,7 +139,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;
@ -132,21 +156,38 @@ 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?
// ######: TODO: clientID needed?
/*
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));
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.
// 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;

View file

@ -13,6 +13,7 @@
#define hifi_DomainAccountManager_h
#include <QtCore/QObject>
#include <QtCore/QUrl>
#include <DependencyManager.h>
@ -22,25 +23,40 @@ class DomainAccountManager : public QObject, public Dependency {
public:
DomainAccountManager();
void setAuthURL(const QUrl& authURL);
void setClientID(const QString& clientID) { _clientID = clientID; }
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, const QString& domainAuthProvider);
void requestAccessToken(const QString& username, const QString& password);
void requestAccessTokenFinished();
signals:
void authRequired();
void loginComplete(const QUrl& authURL);
void authRequired(const QString& domain);
void loginComplete();
void loginFailed();
void logoutComplete();
void newTokens();
private slots:
private:
bool hasValidAccessToken();
bool accessTokenIsExpired();
void setAccessTokenFromJSON(const QJsonObject&);
void setTokensFromJSON(const QJsonObject&, const QUrl& url);
void sendInterfaceAccessTokenToServer();
QUrl _authURL;
QString _clientID;
QString _username; // ####### TODO: Store elsewhere?
QString _access_token; // ####... ""
QString _refresh_token; // ####... ""
};
#endif // hifi_DomainAccountManager_h

View file

@ -550,7 +550,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer<Rec
// output to the log so the user knows they got a denied connection request
// and check and signal for an access token so that we can make sure they are logged in
qCWarning(networking) << "The domain-server denied a connection request: " << reasonMessage << " extraInfo:" << extraInfo;
QString sanitizedExtraInfo = extraInfo.toLower().startsWith("http") ? "" : extraInfo; // Don't log URLs.
qCWarning(networking) << "The domain-server denied a connection request: " << reasonMessage
<< " extraInfo:" << sanitizedExtraInfo;
if (!_domainConnectionRefusals.contains(reasonMessage)) {
_domainConnectionRefusals.insert(reasonMessage);
@ -584,8 +586,13 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer<Rec
}
} else if (reasonSuggestsDomainLogin(reasonCode)) {
qCWarning(networking) << "Make sure you are logged in to the domain.";
auto accountManager = DependencyManager::get<DomainAccountManager>();
if (!extraInfo.isEmpty()) {
auto extraInfoComponents = extraInfo.split("|");
accountManager->setAuthURL(extraInfoComponents.value(0));
accountManager->setClientID(extraInfoComponents.value(1));
}
if (!_hasCheckedForDomainAccessToken) {
accountManager->checkAndSignalForAccessToken();

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

@ -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<DomainAccountManager>();
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<AddressManager>()->getPlaceName();
// ####### TODO: Also send if need to send new domainLogin data?
if (!domainIsConnected) {
DataServerAccountInfo& accountInfo = accountManager->getAccountInfo();
packetStream << accountInfo.getUsername();
@ -477,20 +486,21 @@ 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<DomainAccountManager>();
if (!domainAccountManager->getUsername().isEmpty()) {
packetStream << domainAccountManager->getUsername();
if (!domainAccountManager->getAccessToken().isEmpty()) {
packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken());
}
}
}

View file

@ -196,6 +196,8 @@ private:
#if (PR_BUILD || DEV_BUILD)
bool _shouldSendNewerVersion { false };
#endif
bool _hasDomainAccountManager { false };
};
#endif // hifi_NodeList_h

View file

@ -54,8 +54,6 @@ public:
void setVerifiedDomainUserName(QString userName) { _verifiedDomainUserName = userName.toLower(); }
const QString& getVerifiedDomainUserName() const { return _verifiedDomainUserName; }
void setVerifiedDomainUserGroups(QStringList userGroups) { _verifiedDomainUserGroups = userGroups; }
const QStringList& getVerifiedDomainUserGroups() const { return _verifiedDomainUserGroups; }
void setGroupID(QUuid groupID) { _groupID = groupID; if (!groupID.isNull()) { _groupIDSet = true; }}
QUuid getGroupID() const { return _groupID; }
@ -106,7 +104,6 @@ protected:
QUuid _rankID { QUuid() }; // 0 unless this is for a group
QString _verifiedUserName;
QString _verifiedDomainUserName;
QStringList _verifiedDomainUserGroups;
bool _groupIDSet { false };
QUuid _groupID;