mirror of
https://github.com/overte-org/overte.git
synced 2025-08-06 19:59:28 +02:00
Merge pull request #585 from ctrlaltdavid/feature/oauth2-login
OAuth2 WordPress log in
This commit is contained in:
commit
4bfb414cbe
19 changed files with 470 additions and 113 deletions
|
@ -3,7 +3,7 @@
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"name": "metaverse",
|
"name": "metaverse",
|
||||||
"label": "Metaverse / Networking",
|
"label": "Networking / Metaverse",
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
|
@ -57,6 +57,39 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "authentication",
|
||||||
|
"label": "Networking / WordPress OAuth2",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"name": "enable_oauth2",
|
||||||
|
"label": "Enable OAuth2 Authentication",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oauth2_url_path",
|
||||||
|
"label": "Authentication URL",
|
||||||
|
"help": "The URL 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 \"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Monitoring",
|
"label": "Monitoring",
|
||||||
"name": "monitoring",
|
"name": "monitoring",
|
||||||
|
|
|
@ -90,12 +90,13 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
|
||||||
|
|
||||||
SharedNodePointer node;
|
SharedNodePointer node;
|
||||||
QString username;
|
QString username;
|
||||||
QString domainUsername;
|
|
||||||
if (pendingAssignment != _pendingAssignedNodes.end()) {
|
if (pendingAssignment != _pendingAssignedNodes.end()) {
|
||||||
node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second);
|
node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second);
|
||||||
} else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) {
|
} else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) {
|
||||||
QByteArray usernameSignature;
|
QByteArray usernameSignature;
|
||||||
QByteArray domainUsernameSignature;
|
|
||||||
|
QString domainUsername;
|
||||||
|
QStringList domainTokens;
|
||||||
|
|
||||||
if (message->getBytesLeftToRead() > 0) {
|
if (message->getBytesLeftToRead() > 0) {
|
||||||
// read username from packet
|
// read username from packet
|
||||||
|
@ -108,16 +109,21 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
|
||||||
if (message->getBytesLeftToRead() > 0) {
|
if (message->getBytesLeftToRead() > 0) {
|
||||||
// Read domain username from packet.
|
// Read domain username from packet.
|
||||||
packetStream >> domainUsername;
|
packetStream >> domainUsername;
|
||||||
|
domainUsername = domainUsername.toLower(); // Domain usernames are case-insensitive; internally lower-case.
|
||||||
|
|
||||||
if (message->getBytesLeftToRead() > 0) {
|
if (message->getBytesLeftToRead() > 0) {
|
||||||
// Read domain signature from packet.
|
// Read domain tokens from packet.
|
||||||
packetStream >> domainUsernameSignature;
|
|
||||||
|
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) {
|
if (node) {
|
||||||
|
@ -175,7 +181,7 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
|
||||||
auto userGroups = _domainGroupMemberships[verifiedDomainUserName];
|
auto userGroups = _domainGroupMemberships[verifiedDomainUserName];
|
||||||
foreach (QString userGroup, userGroups) {
|
foreach (QString userGroup, userGroups) {
|
||||||
// Domain groups may be specified as comma- and/or space-separated lists of group names.
|
// 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()
|
auto domainGroups = _server->_settingsManager.getDomainGroupNames()
|
||||||
.filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$",
|
.filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$",
|
||||||
QRegularExpression::CaseInsensitiveOption));
|
QRegularExpression::CaseInsensitiveOption));
|
||||||
|
@ -296,7 +302,7 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
|
||||||
auto userGroups = _domainGroupMemberships[verifiedDomainUserName];
|
auto userGroups = _domainGroupMemberships[verifiedDomainUserName];
|
||||||
foreach(QString userGroup, userGroups) {
|
foreach(QString userGroup, userGroups) {
|
||||||
// Domain groups may be specified as comma- and/or space-separated lists of group names.
|
// 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()
|
auto domainGroups = _server->_settingsManager.getDomainBlacklistGroupNames()
|
||||||
.filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$",
|
.filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$",
|
||||||
QRegularExpression::CaseInsensitiveOption));
|
QRegularExpression::CaseInsensitiveOption));
|
||||||
|
@ -444,6 +450,10 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
|
||||||
return newNode;
|
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 = "security.maximum_user_capacity";
|
||||||
const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location";
|
const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location";
|
||||||
|
|
||||||
|
@ -451,7 +461,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
|
||||||
const QString& username,
|
const QString& username,
|
||||||
const QByteArray& usernameSignature,
|
const QByteArray& usernameSignature,
|
||||||
const QString& domainUsername,
|
const QString& domainUsername,
|
||||||
const QByteArray& domainUsernameSignature) {
|
const QString& domainAccessToken,
|
||||||
|
const QString& domainRefreshToken) {
|
||||||
|
|
||||||
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
|
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
|
||||||
|
|
||||||
|
@ -501,29 +512,33 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
|
||||||
QString verifiedDomainUsername;
|
QString verifiedDomainUsername;
|
||||||
QStringList verifiedDomainUserGroups;
|
QStringList verifiedDomainUserGroups;
|
||||||
if (domainHasLogin() && !domainUsername.isEmpty()) {
|
if (domainHasLogin() && !domainUsername.isEmpty()) {
|
||||||
if (domainUsernameSignature.isEmpty()) {
|
|
||||||
|
if (domainAccessToken.isEmpty()) {
|
||||||
// User is attempting to prove their domain identity.
|
// User is attempting to prove their domain identity.
|
||||||
|
|
||||||
// ####### TODO: OAuth2 corollary of metaverse code, above.
|
|
||||||
|
|
||||||
getDomainGroupMemberships(domainUsernameSignature); // Optimistically get started on group memberships.
|
|
||||||
#ifdef WANT_DEBUG
|
#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
|
#endif
|
||||||
return SharedNodePointer();
|
return SharedNodePointer();
|
||||||
} else if (verifyDomainUserSignature(domainUsername, domainUsernameSignature, nodeConnection.senderSockAddr)) {
|
|
||||||
|
} 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.
|
// User's domain identity is confirmed.
|
||||||
getDomainGroupMemberships(domainUsername);
|
verifiedDomainUsername = domainUsername;
|
||||||
verifiedDomainUsername = domainUsername.toLower();
|
|
||||||
} else {
|
} else {
|
||||||
// User's identity didn't check out.
|
// User's domain identity didn't check out.
|
||||||
|
|
||||||
// ####### TODO: OAuth2 corollary of metaverse code, above.
|
|
||||||
|
|
||||||
#ifdef WANT_DEBUG
|
#ifdef WANT_DEBUG
|
||||||
qDebug() << "stalling login because domain signature verification failed:" << domainUsername;
|
qDebug() << "Stalling login because domain user verification failed:" << domainUsername;
|
||||||
#endif
|
#endif
|
||||||
return SharedNodePointer();
|
return SharedNodePointer();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -533,10 +548,22 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
|
||||||
|
|
||||||
if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) {
|
if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) {
|
||||||
if (domainHasLogin()) {
|
if (domainHasLogin()) {
|
||||||
sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.",
|
QString domainAuthURL;
|
||||||
nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain);
|
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 {
|
} 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);
|
nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedMetaverse);
|
||||||
}
|
}
|
||||||
#ifdef WANT_DEBUG
|
#ifdef WANT_DEBUG
|
||||||
|
@ -734,17 +761,21 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DomainGatekeeper::verifyDomainUserSignature(const QString& domainUsername,
|
|
||||||
const QByteArray& domainUsernameSignature,
|
|
||||||
const HifiSockAddr& senderSockAddr) {
|
|
||||||
|
|
||||||
// ####### TODO: Verify via domain OAuth2.
|
bool DomainGatekeeper::needToVerifyDomainUserIdentity(const QString& username, const QString& accessToken,
|
||||||
bool success = true;
|
const QString& refreshToken) {
|
||||||
if (success) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendConnectionDeniedPacket("Error decrypting domain username signature.", senderSockAddr,
|
sendConnectionDeniedPacket("Error verifying domain user.", senderSockAddr,
|
||||||
DomainHandler::ConnectionRefusedReason::LoginErrorDomain);
|
DomainHandler::ConnectionRefusedReason::LoginErrorDomain);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1076,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.
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
QStringList wordpressGroupsForUser;
|
|
||||||
wordpressGroupsForUser << "silVER" << "gold" << "coal";
|
|
||||||
_domainGroupMemberships[domainUserName] = wordpressGroupsForUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void DomainGatekeeper::getDomainOwnerFriendsList() {
|
void DomainGatekeeper::getDomainOwnerFriendsList() {
|
||||||
JSONCallbackParameters callbackParams;
|
JSONCallbackParameters callbackParams;
|
||||||
callbackParams.callbackReceiver = this;
|
callbackParams.callbackReceiver = this;
|
||||||
|
@ -1138,7 +1155,7 @@ void DomainGatekeeper::getDomainOwnerFriendsListErrorCallback(QNetworkReply* req
|
||||||
qDebug() << "getDomainOwnerFriendsList api call failed:" << requestReply->error();
|
qDebug() << "getDomainOwnerFriendsList api call failed:" << requestReply->error();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ####### TODO: Domain equivalent or addition
|
// ####### TODO: Domain equivalent or addition [plugin groups]
|
||||||
void DomainGatekeeper::refreshGroupsCache() {
|
void DomainGatekeeper::refreshGroupsCache() {
|
||||||
// if agents are connected to this domain, refresh our cached information about groups and memberships in such.
|
// if agents are connected to this domain, refresh our cached information about groups and memberships in such.
|
||||||
getDomainOwnerFriendsList();
|
getDomainOwnerFriendsList();
|
||||||
|
@ -1163,17 +1180,6 @@ void DomainGatekeeper::refreshGroupsCache() {
|
||||||
#endif
|
#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() {
|
void DomainGatekeeper::initLocalIDManagement() {
|
||||||
std::uniform_int_distribution<quint16> sixteenBitRand;
|
std::uniform_int_distribution<quint16> sixteenBitRand;
|
||||||
std::random_device randomDevice;
|
std::random_device randomDevice;
|
||||||
|
@ -1201,3 +1207,91 @@ Node::LocalID DomainGatekeeper::findOrCreateLocalID(const QUuid& uuid) {
|
||||||
_localIDs.insert(newLocalID);
|
_localIDs.insert(newLocalID);
|
||||||
return 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@
|
||||||
#include "NodeConnectionData.h"
|
#include "NodeConnectionData.h"
|
||||||
#include "PendingAssignedNodeData.h"
|
#include "PendingAssignedNodeData.h"
|
||||||
|
|
||||||
|
const QString DOMAIN_GROUP_CHAR = "@";
|
||||||
|
|
||||||
class DomainServer;
|
class DomainServer;
|
||||||
|
|
||||||
class DomainGatekeeper : public QObject {
|
class DomainGatekeeper : public QObject {
|
||||||
|
@ -72,6 +74,10 @@ public slots:
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handlePeerPingTimeout();
|
void handlePeerPingTimeout();
|
||||||
|
|
||||||
|
// Login and groups for domain, separate from metaverse.
|
||||||
|
void requestDomainUserFinished();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SharedNodePointer processAssignmentConnectRequest(const NodeConnectionData& nodeConnection,
|
SharedNodePointer processAssignmentConnectRequest(const NodeConnectionData& nodeConnection,
|
||||||
const PendingAssignedNodeData& pendingAssignment);
|
const PendingAssignedNodeData& pendingAssignment);
|
||||||
|
@ -79,14 +85,16 @@ private:
|
||||||
const QString& username,
|
const QString& username,
|
||||||
const QByteArray& usernameSignature,
|
const QByteArray& usernameSignature,
|
||||||
const QString& domainUsername,
|
const QString& domainUsername,
|
||||||
const QByteArray& domainUsernameSignature);
|
const QString& domainAccessToken,
|
||||||
|
const QString& domainRefreshToken);
|
||||||
SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection);
|
SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection);
|
||||||
|
|
||||||
bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature,
|
bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature,
|
||||||
const HifiSockAddr& senderSockAddr);
|
const HifiSockAddr& senderSockAddr);
|
||||||
|
|
||||||
bool verifyDomainUserSignature(const QString& domainUsername, const QByteArray& domainUsernameSignature,
|
bool needToVerifyDomainUserIdentity(const QString& username, const QString& accessToken, const QString& refreshToken);
|
||||||
const HifiSockAddr& senderSockAddr);
|
bool verifyDomainUserIdentity(const QString& username, const QString& accessToken, const QString& refreshToken,
|
||||||
|
const HifiSockAddr& senderSockAddr);
|
||||||
|
|
||||||
bool isWithinMaxCapacity();
|
bool isWithinMaxCapacity();
|
||||||
|
|
||||||
|
@ -135,20 +143,24 @@ private:
|
||||||
// void getIsGroupMember(const QString& username, const QUuid groupID);
|
// void getIsGroupMember(const QString& username, const QUuid groupID);
|
||||||
void getDomainOwnerFriendsList();
|
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.
|
// Local ID management.
|
||||||
void initLocalIDManagement();
|
void initLocalIDManagement();
|
||||||
using UUIDToLocalID = std::unordered_map<QUuid, Node::LocalID> ;
|
using UUIDToLocalID = std::unordered_map<QUuid, Node::LocalID> ;
|
||||||
using LocalIDs = std::unordered_set<Node::LocalID>;
|
using LocalIDs = std::unordered_set<Node::LocalID>;
|
||||||
LocalIDs _localIDs;
|
LocalIDs _localIDs;
|
||||||
UUIDToLocalID _uuidToLocalID;
|
UUIDToLocalID _uuidToLocalID;
|
||||||
|
|
||||||
Node::LocalID _currentLocalID;
|
Node::LocalID _currentLocalID;
|
||||||
Node::LocalID _idIncrement;
|
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]>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1966,6 +1966,10 @@ void DomainServerSettingsManager::apiRefreshGroupInformation() {
|
||||||
QStringList groupNames = getAllKnownGroupNames();
|
QStringList groupNames = getAllKnownGroupNames();
|
||||||
foreach (QString groupName, groupNames) {
|
foreach (QString groupName, groupNames) {
|
||||||
QString lowerGroupName = groupName.toLower();
|
QString lowerGroupName = groupName.toLower();
|
||||||
|
if (lowerGroupName.contains(DOMAIN_GROUP_CHAR)) {
|
||||||
|
// Ignore domain groups.
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (_groupIDs.contains(lowerGroupName)) {
|
if (_groupIDs.contains(lowerGroupName)) {
|
||||||
// we already know about this one. recall setGroupID in case the group has been
|
// 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).
|
// added to another section (the same group is found in both groups and blacklists).
|
||||||
|
@ -2186,7 +2190,7 @@ QList<QUuid> DomainServerSettingsManager::getBlacklistGroupIDs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList DomainServerSettingsManager::getDomainGroupNames() {
|
QStringList DomainServerSettingsManager::getDomainGroupNames() {
|
||||||
// Names as configured in domain server; not necessarily mnetaverse groups.
|
// Names as configured in domain server; not necessarily metaverse groups.
|
||||||
QSet<QString> result;
|
QSet<QString> result;
|
||||||
foreach(NodePermissionsKey groupKey, _groupPermissions.keys()) {
|
foreach(NodePermissionsKey groupKey, _groupPermissions.keys()) {
|
||||||
result += _groupPermissions[groupKey]->getID();
|
result += _groupPermissions[groupKey]->getID();
|
||||||
|
|
|
@ -19,11 +19,11 @@
|
||||||
|
|
||||||
#include <HifiConfigVariantMap.h>
|
#include <HifiConfigVariantMap.h>
|
||||||
#include <HTTPManager.h>
|
#include <HTTPManager.h>
|
||||||
|
|
||||||
#include <ReceivedMessage.h>
|
|
||||||
#include "NodePermissions.h"
|
|
||||||
|
|
||||||
#include <Node.h>
|
#include <Node.h>
|
||||||
|
#include <ReceivedMessage.h>
|
||||||
|
|
||||||
|
#include "DomainGatekeeper.h"
|
||||||
|
#include "NodePermissions.h"
|
||||||
|
|
||||||
const QString SETTINGS_PATHS_KEY = "paths";
|
const QString SETTINGS_PATHS_KEY = "paths";
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ Item {
|
||||||
|
|
||||||
readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp()
|
readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp()
|
||||||
readonly property bool isLoggingInToDomain: loginDialog.getDomainLoginRequested()
|
readonly property bool isLoggingInToDomain: loginDialog.getDomainLoginRequested()
|
||||||
readonly property string domainAuthProvider: loginDialog.getDomainLoginAuthProvider()
|
readonly property string domainLoginDomain: loginDialog.getDomainLoginDomain()
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: d
|
id: d
|
||||||
|
@ -76,7 +76,7 @@ Item {
|
||||||
if (!isLoggingInToDomain) {
|
if (!isLoggingInToDomain) {
|
||||||
loginDialog.login(emailField.text, passwordField.text);
|
loginDialog.login(emailField.text, passwordField.text);
|
||||||
} else {
|
} else {
|
||||||
loginDialog.loginDomain(emailField.text, passwordField.text, domainAuthProvider);
|
loginDialog.loginDomain(emailField.text, passwordField.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (linkAccountBody.loginDialogPoppedUp) {
|
if (linkAccountBody.loginDialogPoppedUp) {
|
||||||
|
|
|
@ -9444,7 +9444,6 @@ void Application::forceDisplayName(const QString& displayName) {
|
||||||
getMyAvatar()->setDisplayName(displayName);
|
getMyAvatar()->setDisplayName(displayName);
|
||||||
}
|
}
|
||||||
void Application::forceLoginWithTokens(const QString& tokens) {
|
void Application::forceLoginWithTokens(const QString& tokens) {
|
||||||
// ####### TODO
|
|
||||||
DependencyManager::get<AccountManager>()->setAccessTokens(tokens);
|
DependencyManager::get<AccountManager>()->setAccessTokens(tokens);
|
||||||
Setting::Handle<bool>(KEEP_ME_LOGGED_IN_SETTING_NAME, true).set(true);
|
Setting::Handle<bool>(KEEP_ME_LOGGED_IN_SETTING_NAME, true).set(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,10 @@
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
#include "ui/DialogsManager.h"
|
#include "ui/DialogsManager.h"
|
||||||
|
|
||||||
|
#include <AccountManager.h>
|
||||||
#include <DependencyManager.h>
|
#include <DependencyManager.h>
|
||||||
#include <DomainHandler.h>
|
#include <DomainHandler.h>
|
||||||
|
#include <DomainAccountManager.h>
|
||||||
#include <AddressManager.h>
|
#include <AddressManager.h>
|
||||||
#include <NodeList.h>
|
#include <NodeList.h>
|
||||||
|
|
||||||
|
@ -34,6 +36,10 @@ void ConnectionMonitor::init() {
|
||||||
connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ConnectionMonitor::stopTimer);
|
connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ConnectionMonitor::stopTimer);
|
||||||
connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &ConnectionMonitor::stopTimer);
|
connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &ConnectionMonitor::stopTimer);
|
||||||
connect(this, &ConnectionMonitor::setRedirectErrorState, &domainHandler, &DomainHandler::setRedirectErrorState);
|
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);
|
_timer.setSingleShot(true);
|
||||||
if (!domainHandler.isConnected()) {
|
if (!domainHandler.isConnected()) {
|
||||||
|
|
|
@ -110,8 +110,14 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DialogsManager::setDomainLogin(bool isDomainLogin, const QString& domain) {
|
||||||
|
_isDomainLogin = isDomainLogin;
|
||||||
|
_domainLoginDomain = domain;
|
||||||
|
}
|
||||||
|
|
||||||
void DialogsManager::toggleLoginDialog() {
|
void DialogsManager::toggleLoginDialog() {
|
||||||
_isDomainLogin = false;
|
setDomainLogin(false);
|
||||||
LoginDialog::toggleAction();
|
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
|
// ####### 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.
|
// displayed and may be the domain login version.
|
||||||
|
|
||||||
_isDomainLogin = false;
|
setDomainLogin(false);
|
||||||
LoginDialog::showWithSelection();
|
LoginDialog::showWithSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,8 +135,8 @@ void DialogsManager::hideLoginDialog() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void DialogsManager::showDomainLoginDialog() {
|
void DialogsManager::showDomainLoginDialog(const QString& domain) {
|
||||||
_isDomainLogin = true;
|
setDomainLogin(true, domain);
|
||||||
LoginDialog::showWithSelection();
|
LoginDialog::showWithSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ public:
|
||||||
void emitAddressBarShown(bool visible) { emit addressBarShown(visible); }
|
void emitAddressBarShown(bool visible) { emit addressBarShown(visible); }
|
||||||
void setAddressBarVisible(bool addressBarVisible);
|
void setAddressBarVisible(bool addressBarVisible);
|
||||||
bool getIsDomainLogin() { return _isDomainLogin; }
|
bool getIsDomainLogin() { return _isDomainLogin; }
|
||||||
|
QString getDomainLoginDomain() { return _domainLoginDomain; }
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void showAddressBar();
|
void showAddressBar();
|
||||||
|
@ -51,7 +52,7 @@ public slots:
|
||||||
void toggleLoginDialog();
|
void toggleLoginDialog();
|
||||||
void showLoginDialog();
|
void showLoginDialog();
|
||||||
void hideLoginDialog();
|
void hideLoginDialog();
|
||||||
void showDomainLoginDialog();
|
void showDomainLoginDialog(const QString& domain);
|
||||||
void octreeStatsDetails();
|
void octreeStatsDetails();
|
||||||
void lodTools();
|
void lodTools();
|
||||||
void hmdTools(bool showTools);
|
void hmdTools(bool showTools);
|
||||||
|
@ -86,7 +87,9 @@ private:
|
||||||
bool _dialogCreatedWhileShown { false };
|
bool _dialogCreatedWhileShown { false };
|
||||||
bool _addressBarVisible { false };
|
bool _addressBarVisible { false };
|
||||||
|
|
||||||
|
void setDomainLogin(bool isDomainLogin, const QString& domain = "");
|
||||||
bool _isDomainLogin { false };
|
bool _isDomainLogin { false };
|
||||||
|
QString _domainLoginDomain;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // hifi_DialogsManager_h
|
#endif // hifi_DialogsManager_h
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
#include <UserActivityLogger.h>
|
#include <UserActivityLogger.h>
|
||||||
|
|
||||||
#include "AccountManager.h"
|
#include "AccountManager.h"
|
||||||
|
#include "DomainAccountManager.h"
|
||||||
#include "DependencyManager.h"
|
#include "DependencyManager.h"
|
||||||
#include "DialogsManager.h"
|
#include "DialogsManager.h"
|
||||||
#include "Menu.h"
|
#include "Menu.h"
|
||||||
|
@ -40,12 +41,17 @@ const QUrl LOGIN_DIALOG = PathUtils::qmlUrl("OverlayLoginDialog.qml");
|
||||||
|
|
||||||
LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent) {
|
LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent) {
|
||||||
auto accountManager = DependencyManager::get<AccountManager>();
|
auto accountManager = DependencyManager::get<AccountManager>();
|
||||||
|
auto domainAccountManager = DependencyManager::get<DomainAccountManager>();
|
||||||
// the login hasn't been dismissed yet if the user isn't logged in and is encouraged to login.
|
// the login hasn't been dismissed yet if the user isn't logged in and is encouraged to login.
|
||||||
#if !defined(Q_OS_ANDROID)
|
#if !defined(Q_OS_ANDROID)
|
||||||
connect(accountManager.data(), &AccountManager::loginComplete,
|
connect(accountManager.data(), &AccountManager::loginComplete,
|
||||||
this, &LoginDialog::handleLoginCompleted);
|
this, &LoginDialog::handleLoginCompleted);
|
||||||
connect(accountManager.data(), &AccountManager::loginFailed,
|
connect(accountManager.data(), &AccountManager::loginFailed,
|
||||||
this, &LoginDialog::handleLoginFailed);
|
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::loginDialogFocusEnabled, this, &LoginDialog::focusEnabled);
|
||||||
connect(qApp, &Application::loginDialogFocusDisabled, this, &LoginDialog::focusDisabled);
|
connect(qApp, &Application::loginDialogFocusDisabled, this, &LoginDialog::focusDisabled);
|
||||||
connect(this, SIGNAL(dismissedLoginDialog()), qApp, SLOT(onDismissedLoginDialog()));
|
connect(this, SIGNAL(dismissedLoginDialog()), qApp, SLOT(onDismissedLoginDialog()));
|
||||||
|
@ -137,13 +143,9 @@ void LoginDialog::login(const QString& username, const QString& password) const
|
||||||
DependencyManager::get<AccountManager>()->requestAccessToken(username, password);
|
DependencyManager::get<AccountManager>()->requestAccessToken(username, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoginDialog::loginDomain(const QString& username, const QString& password, const QString& domainAuthProvider) const {
|
void LoginDialog::loginDomain(const QString& username, const QString& password) const {
|
||||||
qDebug() << "Attempting to login" << username << "into a domain through" << domainAuthProvider;
|
qDebug() << "Attempting to login" << username << "into a domain";
|
||||||
// ####### TODO
|
DependencyManager::get<DomainAccountManager>()->requestAccessToken(username, password);
|
||||||
// DependencyManager::get<DomainAccountManager>()->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() {
|
void LoginDialog::loginThroughOculus() {
|
||||||
|
@ -426,8 +428,6 @@ bool LoginDialog::getDomainLoginRequested() const {
|
||||||
return DependencyManager::get<DialogsManager>()->getIsDomainLogin();
|
return DependencyManager::get<DialogsManager>()->getIsDomainLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ####### TODO: This method may not be necessary.
|
QString LoginDialog::getDomainLoginDomain() const {
|
||||||
QString LoginDialog::getDomainLoginAuthProvider() const {
|
return DependencyManager::get<DialogsManager>()->getDomainLoginDomain();
|
||||||
// ####### TODO
|
|
||||||
return QString("https://example.com/oauth2");
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ protected slots:
|
||||||
Q_INVOKABLE QString oculusUserID() const;
|
Q_INVOKABLE QString oculusUserID() const;
|
||||||
|
|
||||||
Q_INVOKABLE void login(const QString& username, const QString& password) 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 loginThroughSteam();
|
||||||
Q_INVOKABLE void linkSteam();
|
Q_INVOKABLE void linkSteam();
|
||||||
Q_INVOKABLE void createAccountFromSteam(QString username = QString());
|
Q_INVOKABLE void createAccountFromSteam(QString username = QString());
|
||||||
|
@ -85,7 +85,7 @@ protected slots:
|
||||||
Q_INVOKABLE bool getLoginDialogPoppedUp() const;
|
Q_INVOKABLE bool getLoginDialogPoppedUp() const;
|
||||||
|
|
||||||
Q_INVOKABLE bool getDomainLoginRequested() const;
|
Q_INVOKABLE bool getDomainLoginRequested() const;
|
||||||
Q_INVOKABLE QString getDomainLoginAuthProvider() const;
|
Q_INVOKABLE QString getDomainLoginDomain() const;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -508,7 +508,9 @@ bool AccountManager::checkAndSignalForAccessToken() {
|
||||||
|
|
||||||
if (!hasToken) {
|
if (!hasToken) {
|
||||||
// emit a signal so somebody can call back to us and request an access token given a username and password
|
// 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;
|
return hasToken;
|
||||||
|
|
|
@ -12,27 +12,182 @@
|
||||||
#include "DomainAccountManager.h"
|
#include "DomainAccountManager.h"
|
||||||
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QtCore/QJsonObject>
|
||||||
|
#include <QtCore/QJsonDocument>
|
||||||
|
#include <QtNetwork/QNetworkRequest>
|
||||||
|
#include <QtNetwork/QNetworkReply>
|
||||||
|
|
||||||
|
#include <SettingHandle.h>
|
||||||
|
|
||||||
|
#include "NetworkingConstants.h"
|
||||||
|
#include "NetworkLogging.h"
|
||||||
|
#include "NetworkAccessManager.h"
|
||||||
|
|
||||||
|
// 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", "" };
|
||||||
|
|
||||||
|
|
||||||
DomainAccountManager::DomainAccountManager() {
|
DomainAccountManager::DomainAccountManager() :
|
||||||
|
_authURL(),
|
||||||
|
_username(),
|
||||||
|
_access_token(),
|
||||||
|
_refresh_token()
|
||||||
|
{
|
||||||
|
connect(this, &DomainAccountManager::loginComplete, this, &DomainAccountManager::sendInterfaceAccessTokenToServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
_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.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
if (200 <= httpStatus && httpStatus < 300) {
|
||||||
|
|
||||||
|
// 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.";
|
||||||
|
emit loginFailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Failure.
|
||||||
|
qCDebug(networking) << "Error in response for password grant -" << httpStatus << requestReply->error()
|
||||||
|
<< "-" << rootObject["error"].toString() << rootObject["error_description"].toString();
|
||||||
|
emit loginFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainAccountManager::sendInterfaceAccessTokenToServer() {
|
||||||
|
emit newTokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DomainAccountManager::accessTokenIsExpired() {
|
||||||
|
// ####### TODO: accessTokenIsExpired()
|
||||||
|
return true;
|
||||||
|
/*
|
||||||
|
return domainAccessTokenExpiresIn.get() != -1 && domainAccessTokenExpiresIn.get() <= QDateTime::currentMSecsSinceEpoch();
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool DomainAccountManager::hasValidAccessToken() {
|
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(_authURL.toString());
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// ####### TODO::
|
||||||
|
|
||||||
|
// if (!_isWaitingForTokenRefresh && needsToRefreshToken()) {
|
||||||
|
// refreshAccessToken();
|
||||||
|
// }
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 DomainAccountManager::checkAndSignalForAccessToken() {
|
||||||
bool hasToken = hasValidAccessToken();
|
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) {
|
if (!hasToken) {
|
||||||
// Emit a signal so somebody can call back to us and request an access token given a user name and password.
|
// 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.
|
// 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;
|
return hasToken;
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
#define hifi_DomainAccountManager_h
|
#define hifi_DomainAccountManager_h
|
||||||
|
|
||||||
#include <QtCore/QObject>
|
#include <QtCore/QObject>
|
||||||
|
#include <QtCore/QUrl>
|
||||||
|
|
||||||
#include <DependencyManager.h>
|
#include <DependencyManager.h>
|
||||||
|
|
||||||
|
@ -22,18 +23,40 @@ class DomainAccountManager : public QObject, public Dependency {
|
||||||
public:
|
public:
|
||||||
DomainAccountManager();
|
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();
|
Q_INVOKABLE bool checkAndSignalForAccessToken();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
void requestAccessToken(const QString& username, const QString& password);
|
||||||
|
|
||||||
|
void requestAccessTokenFinished();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void authRequired();
|
void authRequired(const QString& domain);
|
||||||
|
void loginComplete();
|
||||||
|
void loginFailed();
|
||||||
|
void logoutComplete();
|
||||||
|
void newTokens();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool hasValidAccessToken();
|
bool hasValidAccessToken();
|
||||||
|
bool accessTokenIsExpired();
|
||||||
|
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
|
#endif // hifi_DomainAccountManager_h
|
||||||
|
|
|
@ -550,7 +550,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer<Rec
|
||||||
|
|
||||||
// output to the log so the user knows they got a denied connection request
|
// 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
|
// 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)) {
|
if (!_domainConnectionRefusals.contains(reasonMessage)) {
|
||||||
_domainConnectionRefusals.insert(reasonMessage);
|
_domainConnectionRefusals.insert(reasonMessage);
|
||||||
|
@ -584,8 +586,13 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer<Rec
|
||||||
}
|
}
|
||||||
} else if (reasonSuggestsDomainLogin(reasonCode)) {
|
} else if (reasonSuggestsDomainLogin(reasonCode)) {
|
||||||
qCWarning(networking) << "Make sure you are logged in to the domain.";
|
qCWarning(networking) << "Make sure you are logged in to the domain.";
|
||||||
|
|
||||||
auto accountManager = DependencyManager::get<DomainAccountManager>();
|
auto accountManager = DependencyManager::get<DomainAccountManager>();
|
||||||
|
if (!extraInfo.isEmpty()) {
|
||||||
|
auto extraInfoComponents = extraInfo.split("|");
|
||||||
|
accountManager->setAuthURL(extraInfoComponents.value(0));
|
||||||
|
accountManager->setClientID(extraInfoComponents.value(1));
|
||||||
|
}
|
||||||
|
|
||||||
if (!_hasCheckedForDomainAccessToken) {
|
if (!_hasCheckedForDomainAccessToken) {
|
||||||
accountManager->checkAndSignalForAccessToken();
|
accountManager->checkAndSignalForAccessToken();
|
||||||
|
|
|
@ -125,6 +125,7 @@ public:
|
||||||
|
|
||||||
bool isConnected() const { return _isConnected; }
|
bool isConnected() const { return _isConnected; }
|
||||||
void setIsConnected(bool isConnected);
|
void setIsConnected(bool isConnected);
|
||||||
|
|
||||||
bool isServerless() const { return _domainURL.scheme() != URL_SCHEME_HIFI; }
|
bool isServerless() const { return _domainURL.scheme() != URL_SCHEME_HIFI; }
|
||||||
bool getInterstitialModeEnabled() const;
|
bool getInterstitialModeEnabled() const;
|
||||||
void setInterstitialModeEnabled(bool enableInterstitialMode);
|
void setInterstitialModeEnabled(bool enableInterstitialMode);
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
#include "AddressManager.h"
|
#include "AddressManager.h"
|
||||||
#include "Assignment.h"
|
#include "Assignment.h"
|
||||||
#include "AudioHelpers.h"
|
#include "AudioHelpers.h"
|
||||||
|
#include "DomainAccountManager.h"
|
||||||
#include "HifiSockAddr.h"
|
#include "HifiSockAddr.h"
|
||||||
#include "FingerprintUtils.h"
|
#include "FingerprintUtils.h"
|
||||||
|
|
||||||
|
@ -104,6 +105,13 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
|
||||||
// clear our NodeList when logout is requested
|
// clear our NodeList when logout is requested
|
||||||
connect(accountManager.data(), &AccountManager::logoutComplete , this, [this]{ reset("Logged out"); });
|
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
|
// 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::nodeAdded, this, &NodeList::startNodeHolePunch);
|
||||||
connect(this, &LimitedNodeList::nodeSocketUpdated, 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 << _ownerType.load() << publicSockAddr << localSockAddr << _nodeTypesOfInterest.toList();
|
||||||
packetStream << DependencyManager::get<AddressManager>()->getPlaceName();
|
packetStream << DependencyManager::get<AddressManager>()->getPlaceName();
|
||||||
|
|
||||||
|
// ####### TODO: Also send if need to send new domainLogin data?
|
||||||
if (!domainIsConnected) {
|
if (!domainIsConnected) {
|
||||||
DataServerAccountInfo& accountInfo = accountManager->getAccountInfo();
|
DataServerAccountInfo& accountInfo = accountManager->getAccountInfo();
|
||||||
packetStream << accountInfo.getUsername();
|
packetStream << accountInfo.getUsername();
|
||||||
|
@ -477,20 +486,21 @@ void NodeList::sendDomainServerCheckIn() {
|
||||||
const QByteArray& usernameSignature = accountManager->getAccountInfo().getUsernameSignature(connectionToken);
|
const QByteArray& usernameSignature = accountManager->getAccountInfo().getUsernameSignature(connectionToken);
|
||||||
packetStream << usernameSignature;
|
packetStream << usernameSignature;
|
||||||
} else {
|
} 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 {
|
} 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.
|
// Send domain domain login data from Interface to domain server.
|
||||||
// ####### If get into difficulties, could perhaps send domain's username and signature instead of metaverse.
|
if (_hasDomainAccountManager) {
|
||||||
bool domainLoginIsConnected = false;
|
auto domainAccountManager = DependencyManager::get<DomainAccountManager>();
|
||||||
if (!domainLoginIsConnected) {
|
if (!domainAccountManager->getUsername().isEmpty()) {
|
||||||
if (false) { // ####### For testing, false causes user to be considered "not logged in".
|
packetStream << domainAccountManager->getUsername();
|
||||||
packetStream << QString("a@b.c");
|
if (!domainAccountManager->getAccessToken().isEmpty()) {
|
||||||
if (true) { // ####### For testing, false is unhandled at this stage.
|
packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken());
|
||||||
packetStream << QString("signature"); // #######: Consider "logged in" if this is sent during testing.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -196,6 +196,8 @@ private:
|
||||||
#if (PR_BUILD || DEV_BUILD)
|
#if (PR_BUILD || DEV_BUILD)
|
||||||
bool _shouldSendNewerVersion { false };
|
bool _shouldSendNewerVersion { false };
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
bool _hasDomainAccountManager { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // hifi_NodeList_h
|
#endif // hifi_NodeList_h
|
||||||
|
|
Loading…
Reference in a new issue