Hello,
I created a rule as pasted below. This rule is enabled and runs before the default auth0-authorization-extension
rule.
When a new user is signing in with a social account, the rule will look for an existing ‘username-password’ account with the same email address (which had already been created and assigned with groups & roles in the Authorization Extension); and then it will copy the ‘groups’ and ‘roles’ to the current (social) user account; and in the end, delete the original ‘username-password’ account.
Most things are already working fine with this rule. However, I found that from time to time, the “Add User to Groups” API call would only partially succeed (having returned HTTP 204 nevertheless). By ‘partial success’, I mean that when trying to add the user to multiple, only some groups would be added and the other not.
Can someone please help check why this is happening? I can clearly see the HTTP 204 success response for the requests in the rule debug logs like:
[23:16:50.021Z] INFO wt: 170920/231647.304, [response] socket:/data/io/port.sock: patch /api/users/google-apps%7Cxxxxx@xxxxx.com/groups {} 204 (2566ms)
However, sometimes it just adds some of all the groups I provided in request body.
The entire rule is pasted below. EXTENSION_URL
, RULE_CLIENT_ID
and RULE_CLIENT_SECRET
need to be populated with correct values for the rule to run. And the non-interactive api-client needs to be configured with enough scopes under ‘auth0-authorization-extension-api’ and ‘delete:users’ under ‘Auth0 Management API’.
function(user, context, callback) {
var audience = '';
audience = audience || (context.request && context.request.query && context.request.query.audience);
if (audience === 'urn:auth0-authz-api') {
return callback(new UnauthorizedError('no_end_users'));
}
audience = audience || (context.request && context.request.body && context.request.body.audience);
if (audience === 'urn:auth0-authz-api') {
return callback(new UnauthorizedError('no_end_users'));
}
// Modify these based on tenant URL & client configuration:
var EXTENSION_URL = 'https://[tenant-subdomain].webtask.io/[authorization-extension-key]/api';
var RULE_CLIENT_ID = '[api-client-id]';
var RULE_CLIENT_SECRET = '[api-client-secret]';
var TENANT_ORIGIN = 'https://' + context.request.hostname;
var CREDENTIAL_EXCHANGE_URL = TENANT_ORIGIN + '/oauth/token';
var MANAGEMENT_URL = TENANT_ORIGIN + '/api/v2';
var RULE_CACHE_KEY = 'auth0_merge_authz_rule';
var Promise = require('promise');
var request = require('request@2.56.0');
var async = require('async@2.1.2');
var jwt = require('jsonwebtoken');
var _ = require('lodash');
var userApiUrl = auth0.baseUrl + '/users';
var baseApiClientRequestOpts = {
json: true,
timeout: 10000
};
function getCached(key) {
var cachedValue = (global[RULE_CACHE_KEY] || {})[key];
console.log('### returning cached value for key', key, ' => ', cachedValue);
return cachedValue;
}
function cache(key, val) {
var ruleCache = global[RULE_CACHE_KEY];
if (!ruleCache) {
ruleCache = global[RULE_CACHE_KEY] = {};
}
ruleCache[key] = val;
// console.log('### caching global[RULE_CACHE_KEY] => ', global[RULE_CACHE_KEY]);// Uncomment for debugging
}
function getApiToken(audience) {
var cachedToken = getCached(audience);
if (cachedToken) {
var decoded = jwt.decode(cachedToken);
if ((decoded.exp - 60) * 1000 > Date.now()) {
return Promise.resolve(cachedToken);
}
}
var options = {
json: true,
url: CREDENTIAL_EXCHANGE_URL,
body: {
client_id: RULE_CLIENT_ID,
client_secret: RULE_CLIENT_SECRET,
audience: audience,
grant_type: 'client_credentials'
}
};
return new Promise(function(resolve, reject) {
request.post(options, getResponseHandler(resolve, reject));
}).then(function(data) {
var accessToken = data.access_token;
cache(audience, accessToken);
return accessToken;
});
}
function getAuthzApiToken() {
return getApiToken('urn:auth0-authz-api');
}
function getManagementApiToken() {
return getApiToken(MANAGEMENT_URL+ '/');// The management-api 'audience' string must have a trailing `/` (slash)
}
function getApiClientRequestOptions(url, accessToken, body) {
return Object.assign({}, baseApiClientRequestOpts, {
url: url,
headers: {
'Authorization': 'Bearer ' + accessToken
},
body: body
});
}
function getResponseHandler(resolve, reject) {
return function(error, res, data) {
if (error) {
return reject(error);
}
if ((res.statusCode + '').slice(0, 1) !== '2') {//If code is not 2xx
return reject(res.body || res.statusCode);
}
return resolve(data);
};
}
function deleteUser(accessToken, userId) {
return new Promise(function(resolve, reject) {
request.del(
getApiClientRequestOptions([MANAGEMENT_URL, 'users', encodeURIComponent(userId)].join('/'), accessToken),
getResponseHandler(resolve, reject)
);
});
}
function getUserGroups(accessToken, userId) {
return new Promise(function(resolve, reject) {
request.get(
getApiClientRequestOptions([EXTENSION_URL, 'users', encodeURIComponent(userId), 'groups'].join('/'), accessToken),
getResponseHandler(resolve, reject)
);
});
}
function getUserRoles(accessToken, userId) {
return new Promise(function(resolve, reject) {
request.get(
getApiClientRequestOptions([EXTENSION_URL, 'users', encodeURIComponent(userId), 'roles'].join('/'), accessToken),
getResponseHandler(resolve, reject)
);
});
}
function addUserToGroups(accessToken, userId, groupIds) {
var url = [EXTENSION_URL, 'users', encodeURIComponent(userId), 'groups'].join('/');
console.log('### CALLING addUserToGroups with url => ', url, ' and groupIds => ', groupIds);
return new Promise(function(resolve, reject) {
request.patch(
getApiClientRequestOptions(url, accessToken, groupIds),
getResponseHandler(resolve, reject)
);
});
}
function addUserToRoles(accessToken, userId, roleIds) {
var url = [EXTENSION_URL, 'users', encodeURIComponent(userId), 'roles'].join('/');
console.log('### CALLING addUserToRoles with url => ', url, ' and roleIds => ', roleIds);
return new Promise(function(resolve, reject) {
request.patch(
getApiClientRequestOptions(url, accessToken, roleIds),
getResponseHandler(resolve, reject)
);
});
}
function deleteGroupMembers(accessToken, groupId, userIds) {
var url = [EXTENSION_URL, 'groups', groupId, 'members'].join('/');
console.log('### CALLING deleteGroupMembers with url => ', url, ' and userIds => ', userIds);
return new Promise(function(resolve, reject) {
request.del(
getApiClientRequestOptions(url, accessToken, userIds),
getResponseHandler(resolve, reject)
);
});
}
function removeUserFromRoles(accessToken, userId, roleIds) {
var url = [EXTENSION_URL, 'users', encodeURIComponent(userId), 'roles'].join('/');
console.log('### CALLING removeUserFromRoles with url => ', url, ' and roleIds => ', roleIds);
return new Promise(function(resolve, reject) {
request.del(
getApiClientRequestOptions(url, accessToken, roleIds),
getResponseHandler(resolve, reject)
);
});
}
function proceedToNext() {
return callback(undefined, user, context);
}
function getArrayMapper(field) {
return function(arr) {
return arr.map(function(item) {
return item[field];
});
};
}
function getUsernamePasswordUser(user) {
return new Promise(function(resolve, reject) {
request({
json: true,
url: userApiUrl,
headers: {
Authorization: 'Bearer ' + auth0.accessToken
},
qs: {
search_engine: 'v2',
q: 'email.raw:"', user.email, '" -user_id:"', user.user_id, '"'].join('')
}
},
function(error, res, data) {
if (error) {
return reject(error);
}
if (res.statusCode !== 200) {
return reject(res.statusCode);
}
if (data.length > 0) {
async.filter(data, function(targetUser, filterCb) {
filterCb(null, targetUser.identities[0].connection === 'Username-Password-Authentication');
}, function(error, results) {
if (error) {
return reject(error);
}
if (results.length === 1) {
return resolve(results[0]);
}
reject(null);
});
} else {
reject(null);
}
});
});
}
// If `app_metadata.authorization` had already been assigned, it means this is not the first time user logged in
if (user.app_metadata && user.app_metadata.authorization) {
return proceedToNext();
}
var userId = user.user_id;
getUsernamePasswordUser(user)
.catch(function() {
var errorMsg = 'Authorization Extension: No username password account found.';
console.warn(errorMsg + ' For user: ', user);
return Promise.reject(errorMsg);
})
.then(function(originalUser) {
return getAuthzApiToken()
.then(function(accessToken) {
var originalUserId = originalUser.user_id;
function deleteOriginalUserAsync(results) {
var groupIds = results[0];
var roleIds = results[1];
var deleteUserFromGroup = _.bind(deleteGroupMembers, undefined, accessToken, _, [originalUserId]);
groupIds.reduce(function(promise, groupId) {
return promise.then(deleteUserFromGroup(groupId));
}, Promise.resolve())
.then(removeUserFromRoles.bind(undefined, accessToken, originalUserId, roleIds))
.then(function() {
return getManagementApiToken()
.then(_.bind(deleteUser, undefined, _, originalUserId));
});
// Do not return promise
}
function copyUserGroups() {
return getUserGroups(accessToken, originalUserId)
.then(getArrayMapper('_id'))
.then(function(groupIds) {
if (!groupIds.length) {
return groupIds;
}
return addUserToGroups(accessToken, userId, groupIds)
.then(function() { return groupIds; });
});
}
function copyUserRoles() {
return getUserRoles(accessToken, originalUserId)
.then(getArrayMapper('_id'))
.then(function(roleIds) {
if (!roleIds.length) {
return roleIds;
}
return addUserToRoles(accessToken, userId, roleIds)
.then(function() { return roleIds; });
});
}
return Promise.all([copyUserGroups(), copyUserRoles()])
.then(deleteOriginalUserAsync);
});
})
.then(proceedToNext)
.catch(function(error) {
console.error(error);
return callback(new UnauthorizedError(error));
});
}