Skip to content

Instantly share code, notes, and snippets.

@nicosabena
Last active March 3, 2022 18:48
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nicosabena/235c864935711712682baca19537634c to your computer and use it in GitHub Desktop.
Save nicosabena/235c864935711712682baca19537634c to your computer and use it in GitHub Desktop.
Auth0 rule to get user groups from Azure AD
// This rule will get the groups for users coming from Azure AD
// Auth0 already has the option to do that, but it (currently) won't work
// if the user is coming from a different directory than the directory
// where the app is registered (this can happen with multi-tenant apps).
// It uses the access_token provided by Azure AD, so this needs
// the 'Open ID Connect' protocol selected in the Azure AD connection.
//
// After the rule runs, you will have the 'groups' property in the user
// that you can use to add custom claims to the id_token.
//
// Currently, the rule just reads the top-level groups:
// waadClient.getGroupsForUserByObjectIdOrUpn(user.oid, function(err, groups) {
// but you can change it to read the extended user profile:
// waadClient.getUserByProperty('objectId', user.oid, { includeGroups: true, includeNestedGroups: false }, function(err, extUser) {
//
// To set up:
// - [Create a non-interactive client](https://auth0.com/docs/clients/client-settings/non-interactive)
// - Authorize the client to use Management API v2 with the `read:users` and `read:user_idp_tokens` scopes.
// - Configure your domain, client id and client secret in the rule (line 12-17)
// You can use the [configuration object](https://auth0.com/docs/rules/current#using-the-configuration-object)
// to store those values.
function (user, context, callback) {
'use strict';
if (context.connectionStrategy !== 'waad') {
// don't execute for non Azure AD connections
return callback(null, user, context);
}
if (user.groups) {
// dont't execute if user groups were already retrieved
return callback(null, user, context);
}
// you Auth0 domain
var AUTH0_DOMAIN = 'fabrikam.auth0.com';
// the client id and secret of a non-interactive client that is
// authorized to Management API v2 with scopes 'read:users' and 'read:user_idp_tokens'.
var APIV2_CLIENT_ID = 'xxxx';
var APIV2_CLIENT_SECRET = 'xxxx';
var auth0 = require('auth0@2.6.0');
var request = require('request');
var async = require('async');
function Waad10(options) {
options = options || {};
this.options = options;
this.baseUrl = 'https://graph.windows.net/';
this.apiVersion = '1.0';
if (!this.options.tenant) {
throw new Error('Must supply "tenant" id (16a88858-..2d0263900406) or domain (mycompany.onmicrosoft.com)');
}
if (!this.options.accessToken) {
throw new Error('Must supply "accessToken"');
}
}
Waad10.prototype.getUserByProperty = function (propertyName, propertyValue, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
var qs = {
"$filter": propertyName + " eq '" + propertyValue + "'",
"$top": 1
};
return this.__queryUsers(qs, options, function (err, users) {
if (err) return callback(err);
return callback(null, users[0]);
});
};
Waad10.prototype.__queryUsers = function (qs, options, callback) {
if (typeof options === 'function') {
callback = options;
options = options || {};
}
qs['api-version'] = this.apiVersion;
this.__request({
url: this.baseUrl + this.options.tenant + '/users',
qs: qs,
}, function (err, users) {
if (err || !users) return callback(err);
if (!options || !options.includeGroups) return callback(null, users);
async.forEach(users, function (user, cb) {
var groupsCallback = function (err, groups) {
if (err) return callback(err);
user.groups = groups;
cb();
};
if (options.includeNestedGroups) {
this.__queryNestedUserGroups(user.objectId, groupsCallback);
} else {
this.__queryUserGroup(user.objectId, groupsCallback);
}
}.bind(this), function (err) {
if (err) return callback(err);
return callback(null, users);
});
}.bind(this));
};
Waad10.prototype.__queryUserGroup = function (objectIdOrUpn, cb) {
var qs = {
"api-version": this.apiVersion
// Uncomment if you want to change the default max of 100 groups returned
//,"$top": 250
};
this.__request({
url: this.baseUrl + this.options.tenant + '/users/' + objectIdOrUpn + '/memberOf',
qs: qs
}, cb);
};
Waad10.prototype.__queryNestedUserGroups = function (objectIdOrUpn, cb) {
var qs = {
"api-version": this.apiVersion
// Uncomment if you want to change the default max of 100 groups returned
//,"$top": 250
};
this.__request({
url: this.baseUrl + this.options.tenant + '/users/' + objectIdOrUpn + '/getMemberGroups',
qs: qs,
method: 'POST',
json: { "securityEnabledOnly": false }
}, function (err, groups) {
if (err || !groups) return cb(err);
return this.__getObjectsByObjectIds(groups, cb);
}.bind(this));
};
Waad10.prototype.__getObjectsByObjectIds = function (objectIds, cb) {
var qs = {};
qs['api-version'] = '1.6';
this.__request({
url: this.baseUrl + this.options.tenant + '/getObjectsByObjectIds',
qs: qs,
method: 'POST',
json: {
"objectIds": objectIds,
"types": ["group"]
}
}, cb);
};
Waad10.prototype.__request = function (options, callback) {
var headers = {
'Authorization': 'Bearer ' + this.options.accessToken,
};
request({
url: options.url,
qs: options.qs || {},
method: options.method || 'GET',
json : options.json,
headers: headers
}, function(err, resp, body) {
if (err) return callback(err, null);
if (resp.statusCode !== 200) {
return callback(new Error(body), null);
}
var array = body;
if (typeof body === 'string'){
try {
array = JSON.parse(body);
} catch (e){
return callback(new Error('Failed to parse response from graph API'));
}
}
if (!array && array.value && array.value.length === 0)
return callback();
return callback(null, array.value);
}.bind(this));
};
Waad10.prototype.getGroupsForUserByObjectIdOrUpn = function(objectIdOrUpn, callback) {
return this.__queryUserGroup(objectIdOrUpn, callback);
};
function getManagementApiAccessToken(cb) {
var cacheKey = "apiv2TokenToReadUserIdpToken";
var cachedToken = global[cacheKey];
if (cachedToken) {
if (cachedToken.expirationDate > new Date()) {
console.log("Reusing cached token that expires in "+ cachedToken.expirationDate);
return cb(null, cachedToken.token);
}
console.log('Cached token found but expired.');
} else {
console.log('Cached token not found.');
}
var authClient = new auth0.AuthenticationClient({
domain: AUTH0_DOMAIN,
clientId: APIV2_CLIENT_ID,
clientSecret: APIV2_CLIENT_SECRET
});
console.log("Getting new api v2 access token");
authClient.clientCredentialsGrant({
audience: 'https://' + AUTH0_DOMAIN + '/api/v2/'
}, function (err, response) {
if (err) {
return cb(err);
}
var expirationDate = new Date();
expirationDate.setSeconds(expirationDate.getSeconds() + response.expires_in - 60);
// store token in cache
console.log("Storing token, expires in "+ expirationDate);
global[cacheKey] = {
expirationDate: expirationDate,
token: response.access_token
};
cb(null, response.access_token);
});
}
function getWaadAccessToken(apiToken, cb) {
var management = new auth0.ManagementClient({
token: apiToken,
domain: AUTH0_DOMAIN
});
management.users.get({ id: user.user_id })
.then(function (read_user) {
var waadIdentity = read_user.identities.find(function(identity) { return identity.provider === 'waad';});
if (waadIdentity) {
if (waadIdentity.access_token) {
return cb(null, waadIdentity.access_token);
} else {
return cb(new Error("Could not find waad access token."));
}
} else {
return cb(new Error("Could not find waad identity."));
}
})
.catch(function (err) {
return cb(err);
});
}
getManagementApiAccessToken(function (err, token) {
if (err) {
return callback(err);
}
getWaadAccessToken(token, function (err, waadToken) {
if (err) {
return callback(err);
}
var waadClient = new Waad10({ accessToken: waadToken, tenant: user.tenantid });
waadClient.getGroupsForUserByObjectIdOrUpn(user.oid, function(err, groups) {
if (err) {
return callback(err);
}
user.groups = [];
if (groups) {
groups.forEach(function (group) {
user.groups.push(group.displayName);
});
}
callback(null, user, context);
});
});
});
}
@thehappycoder
Copy link

Alternatively:

function (user, context, callback) {
  if (user.identities && user.identities.length > 0 && user.identities[0].connection === 'your-waad' && user.groups) {
    const namespace = 'https://some domain name/';
    context.accessToken[namespace + 'groups'] = user.groups; // Or idToken if you need
  }
  
  callback(null, user, context);
}

@nicosabena
Copy link
Author

Alternatively:

function (user, context, callback) {
  if (user.identities && user.identities.length > 0 && user.identities[0].connection === 'your-waad' && user.groups) {
    const namespace = 'https://some domain name/';
    context.accessToken[namespace + 'groups'] = user.groups; // Or idToken if you need
  }
  
  callback(null, user, context);
}

The purpose of the original gist is to retrieve the users' group information (user.groups) when it wasn't available in the first place. The snippet you added is to include the groups in the generated accessToken, which is a valid but separate concern.

@dhmistry3
Copy link

How do I get the group IDs? the user.groups is just a list of the names.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment