├── .gitignore ├── lib ├── index.js ├── errors.js ├── waad10.js ├── auth.js └── waad.js ├── test ├── config.js ├── auth.tests.js ├── paging.tests.js ├── waad0.5.tests.js └── waad1.0.tests.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./auth'); 2 | module.exports.GraphClient = require('./waad'); 3 | module.exports.GraphClient10 = require('./waad10'); -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | 3 | var AbstractError = function (msg, constr) { 4 | Error.captureStackTrace(this, constr || this); 5 | this.message = msg || 'Error'; 6 | } 7 | 8 | util.inherits(AbstractError, Error); 9 | AbstractError.prototype.name = 'Abstract Error'; 10 | 11 | var OAuthError = function (msg, details) { 12 | OAuthError.super_.call(this, msg, this.constructor); 13 | this.details = details; 14 | } 15 | 16 | util.inherits(OAuthError, AbstractError); 17 | OAuthError.prototype.name = 'OAuth Error'; 18 | 19 | module.exports.AbstractError = AbstractError; 20 | module.exports.OAuthError = OAuthError; 21 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | // for v1 accesss_tokens tests 2 | module.exports.TENANTID = process.env.TENANTID || 'auth10dev.onmicrosoft.com'; 3 | module.exports.APPPRINCIPALID = process.env.APPPRINCIPALID || '2829c758-2bef-1ae8-a685-717089474509'; 4 | module.exports.SYMMETRICKEY = process.env.SYMMETRICKEY || 'FStnXT1QON84B5o38aEmFdlNhEnYtzJ91Gg/JH/Jxiw='; 5 | 6 | // for waad v2 tests 7 | module.exports.WAAD_TENANTDOMAIN = process.env.WAAD_TENANTDOMAIN || 'auth0waadtests.onmicrosoft.com'; 8 | module.exports.WAAD_CLIENTID = process.env.WAAD_CLIENTID || '53e15015-2aa1-4f81-b03f-9c68c4b09908'; 9 | module.exports.WAAD_CLIENTSECRET = process.env.WAAD_CLIENTSECRET || 'ldfKgp2lzHb/9beUSJrAKm8kWCFNSmAbPj0/zAH4a3k='; 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-waad", 3 | "version": "2.1.1", 4 | "description": "query windows azure active directory", 5 | "main": "./lib", 6 | "scripts": { 7 | "test": "mocha -t 10000 -R spec --require should" 8 | }, 9 | "repository": "https://github.com/auth0/node-waad", 10 | "devDependencies": { 11 | "mocha": "*", 12 | "should": "~1.2.1", 13 | "lodash": "~1.0.0-rc.3" 14 | }, 15 | "keywords": [ 16 | "waad", 17 | "azure" 18 | ], 19 | "author": "Matias Woloski", 20 | "license": "MIT", 21 | "dependencies": { 22 | "request": "~2.11.4", 23 | "jwt-simple": "~0.1.0", 24 | "moment": "~1.7.2", 25 | "async": "~0.1.22", 26 | "xtend": "~1.0.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/auth.tests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , auth = require("../lib/auth") 3 | , config = require('./config'); 4 | 5 | describe('login to waad', function () { 6 | it('should obtain an access token', function (done) { 7 | auth.getAccessToken(config.TENANTID, config.APPPRINCIPALID, config.SYMMETRICKEY, function(err, token) { 8 | if (err) { 9 | console.log(err); 10 | } 11 | 12 | assert.notEqual(null, token); 13 | done(); 14 | }); 15 | }); 16 | 17 | it('should fail for wrong tenantId', function (done) { 18 | auth.getAccessToken('wrong-tenant-id', config.APPPRINCIPALID, config.SYMMETRICKEY, function(err, token) { 19 | assert.equal('The requested namespace does not exist.', err.message); 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should fail for wrong service principal', function (done) { 25 | auth.getAccessToken(config.TENANTID, 'wrong-principal', config.SYMMETRICKEY, function(err, token) { 26 | assert.ok(err.message.indexOf('ACS50027') > -1); 27 | done(); 28 | }); 29 | }); 30 | 31 | it('should fail for wrong service key', function (done) { 32 | auth.getAccessToken(config.TENANTID, config.APPPRINCIPALID, 'wrong-key', function(err, token) { 33 | assert.ok(err.message.indexOf('ACS50027') > -1); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /test/paging.tests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , auth = require("../lib/auth") 3 | , Waad = require("../lib/waad") 4 | , config = require('./config') 5 | , _ = require('lodash'); 6 | 7 | 8 | function mapNames (u) { return u.DisplayName; } 9 | 10 | describe('query graph', function () { 11 | before(function(done) { 12 | this.tenant = config.TENANTID; 13 | this.mail = 'matias@auth10dev.onmicrosoft.com'; 14 | 15 | auth.getAccessToken(config.TENANTID, config.APPPRINCIPALID, config.SYMMETRICKEY, function(err, token) { 16 | this.accessToken = token; 17 | done(); 18 | }.bind(this)); 19 | }); 20 | 21 | it('should return the skiptoken and hasMorePages token', function (done) { 22 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 23 | waad.getUsers({ top: 2 }, function(err, users) { 24 | if(err) return done(err); 25 | assert.notEqual(null, users.skiptoken); 26 | users.hasMorePages.should.be.true; 27 | users.length.should.eql(2); 28 | done(); 29 | }.bind(this)); 30 | }); 31 | 32 | it('can get next page with skiptoken', function(done){ 33 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 34 | waad.getUsers({ top: 2 }, function(err, users) { 35 | if(err) return done(err); 36 | var firstPageNames = users.map(mapNames); 37 | 38 | waad.getUsers({ top: 2, skiptoken: users.skiptoken }, function(err, users) { 39 | var secondPageNames = users.map(mapNames); 40 | 41 | _.uniq(firstPageNames.concat(secondPageNames)) 42 | .length.should.eql(4); 43 | 44 | done(); 45 | }); 46 | 47 | }.bind(this)); 48 | }); 49 | 50 | it('can get next page with nextpage method', function(done){ 51 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 52 | waad.getUsers({ top: 2 }, function(err, users) { 53 | if(err) return done(err); 54 | var firstPageNames = users.map(mapNames); 55 | 56 | users.nextPage(function(err, users) { 57 | var secondPageNames = users.map(mapNames); 58 | 59 | _.uniq(firstPageNames.concat(secondPageNames)) 60 | .length.should.eql(4); 61 | 62 | done(); 63 | }); 64 | 65 | }.bind(this)); 66 | }); 67 | 68 | it('should return with hasMorePages false when running out of pages', function(done){ 69 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 70 | waad.getUsers({ top: 2 }, function(err, users) { 71 | if(err) return done(err); 72 | users.nextPage(function(err, users) { 73 | users.nextPage(function(err, users){ 74 | users.hasMorePages.should.be.false; 75 | done(); 76 | }); 77 | }); 78 | 79 | }.bind(this)); 80 | }); 81 | 82 | it('should return all users when all is true', function(done){ 83 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 84 | waad.getUsers({ top: 2, all: true }, function(err, users) { 85 | if(err) return done(err); 86 | users.length.should.eql(5); 87 | done(); 88 | }.bind(this)); 89 | }); 90 | 91 | }); -------------------------------------------------------------------------------- /test/waad0.5.tests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , auth = require("../lib/auth") 3 | , Waad = require("../lib/waad") 4 | , config = require('./config'); 5 | 6 | 7 | describe('query graph', function () { 8 | before(function(done) { 9 | this.tenant = config.TENANTID; 10 | this.upn = 'matias@auth10dev.onmicrosoft.com'; 11 | 12 | auth.getAccessToken(config.TENANTID, config.APPPRINCIPALID, config.SYMMETRICKEY, function(err, token) { 13 | this.accessToken = token; 14 | done(); 15 | }.bind(this)); 16 | }); 17 | 18 | allQueryTests.bind(this)(); 19 | }); 20 | 21 | describe('query graph using token obtained with new WAAD release 2013-04', function () { 22 | before(function(done) { 23 | 24 | this.tenant = config.WAAD_TENANTDOMAIN; 25 | this.upn = 'matias@auth0waadtests.onmicrosoft.com'; 26 | 27 | auth.getAccessTokenWithClientCredentials2(config.WAAD_TENANTDOMAIN, config.WAAD_CLIENTID, config.WAAD_CLIENTSECRET, function(err, token) { 28 | this.accessToken = token; 29 | done(); 30 | }.bind(this)); 31 | }); 32 | 33 | allQueryTests.bind(this)(); 34 | }); 35 | 36 | function allQueryTests () { 37 | it('should get user by email', function (done) { 38 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 39 | waad.getUserByProperty('UserPrincipalName', this.upn, function(err, user) { 40 | if(err) return done(err); 41 | assert.notEqual(null, user); 42 | assert.equal(this.upn, user.UserPrincipalName); 43 | assert.equal('Matias Woloski', user.DisplayName); 44 | done(); 45 | }.bind(this)); 46 | }); 47 | 48 | it('should return null if user not found', function (done) { 49 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 50 | waad.getUserByProperty('UserPrincipalName', 'nonexising@auth10dev.onmicrosoft.com', function(err, user) { 51 | assert.equal(null, user); 52 | done(); 53 | }); 54 | }); 55 | 56 | it('should fail if accessToken is wrong', function (done) { 57 | var waad = new Waad({tenant: this.tenant, accessToken: 'foobarbazbarbiz'}); 58 | waad.getUserByProperty('UserPrincipalName', 'nonexising@auth10dev.onmicrosoft.com', function(err) { 59 | assert.notEqual(null, err); 60 | assert.equal('Authentication_MissingOrMalformed', JSON.parse(err.message).error.code); 61 | done(); 62 | }); 63 | }); 64 | 65 | it('should get groups by user upn', function (done) { 66 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 67 | waad.getGroupsForUserByProperty('UserPrincipalName', this.upn, function(err, groups) { 68 | assert.notEqual(null, groups); 69 | ['Test Group', 'Company Administrator'].forEach(function (group) { 70 | assert.equal(1, groups.filter(function(g){ return g.DisplayName === group; }).length, group); 71 | }); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('can get all users', function (done) { 77 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 78 | waad.getUsers(function(err, users) { 79 | if (err) return done(err); 80 | assert.notEqual(null, users); 81 | var length = users.filter(function(u){ 82 | return u.UserPrincipalName === this.upn; 83 | }.bind(this)).length; 84 | 85 | assert.equal(1, length); 86 | 87 | done(); 88 | }.bind(this)); 89 | }); 90 | 91 | it('should get user with groups by arbitrary property', function (done) { 92 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 93 | waad.getUserByProperty('UserPrincipalName', this.upn, true, function(err, user) { 94 | assert.notEqual(null, user); 95 | assert.equal(this.upn, user.UserPrincipalName); 96 | assert.equal('Matias Woloski', user.DisplayName); 97 | assert.notEqual(null, user.groups); 98 | ['Test Group', 'Company Administrator'].forEach(function (group) { 99 | assert.equal(1, user.groups.filter(function(g){ return g.DisplayName === group; }).length, group); 100 | }); 101 | done(); 102 | }.bind(this)); 103 | }); 104 | 105 | } 106 | -------------------------------------------------------------------------------- /test/waad1.0.tests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , auth = require("../lib/auth") 3 | , Waad = require("../lib/waad10") 4 | , config = require('./config'); 5 | 6 | 7 | describe('query graph api-version 1.0', function () { 8 | before(function(done) { 9 | 10 | this.tenant = config.WAAD_TENANTDOMAIN; 11 | this.upn = 'matias@auth0waadtests.onmicrosoft.com'; 12 | 13 | auth.getAccessTokenWithClientCredentials2(config.WAAD_TENANTDOMAIN, config.WAAD_CLIENTID, config.WAAD_CLIENTSECRET, function(err, token) { 14 | this.accessToken = token; 15 | done(); 16 | }.bind(this)); 17 | }); 18 | 19 | allQueryTests.bind(this)(); 20 | }); 21 | 22 | function allQueryTests () { 23 | it('should get user by upn', function (done) { 24 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 25 | waad.getUserByProperty('userPrincipalName', this.upn, function(err, user) { 26 | if(err) return done(err); 27 | assert.notEqual(null, user); 28 | assert.equal(this.upn, user.userPrincipalName); 29 | assert.equal('Matias Woloski', user.displayName); 30 | assert.equal(undefined, user.groups); 31 | done(); 32 | }.bind(this)); 33 | }); 34 | 35 | it('should return null if user not found', function (done) { 36 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 37 | waad.getUserByProperty('userPrincipalName', 'nonexising@auth10dev.onmicrosoft.com', function(err, user) { 38 | assert.equal(null, user); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('should fail if accessToken is wrong', function (done) { 44 | var waad = new Waad({tenant: this.tenant, accessToken: 'foobarbazbarbiz'}); 45 | waad.getUserByProperty('userPrincipalName', 'nonexising@auth10dev.onmicrosoft.com', function(err) { 46 | assert.notEqual(null, err); 47 | assert.equal('Authentication_MissingOrMalformed', JSON.parse(err.message)['odata.error'].code); 48 | done(); 49 | }); 50 | }); 51 | 52 | it('should get groups by user upn', function (done) { 53 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 54 | waad.getGroupsForUserByObjectIdOrUpn(this.upn, function(err, groups) { 55 | assert.notEqual(null, groups); 56 | ['Test Group', 'Company Administrator'].forEach(function (group) { 57 | assert.equal(1, groups.filter(function(g){ return g.displayName === group; }).length, group); 58 | }); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('can get all users', function (done) { 64 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 65 | waad.getUsers(function(err, users) { 66 | if (err) return done(err); 67 | assert.notEqual(null, users); 68 | var length = users.filter(function(u){ 69 | return u.userPrincipalName === this.upn; 70 | }.bind(this)).length; 71 | 72 | assert.equal(1, length); 73 | 74 | done(); 75 | }.bind(this)); 76 | }); 77 | 78 | it('should get user with groups by arbitrary property', function (done) { 79 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 80 | waad.getUserByProperty('userPrincipalName', this.upn, true, function(err, user) { 81 | assert.notEqual(null, user); 82 | assert.equal(this.upn, user.userPrincipalName); 83 | assert.equal('Matias Woloski', user.displayName); 84 | assert.notEqual(undefined, user.groups); 85 | ['Test Group', 'Company Administrator'].forEach(function (group) { 86 | assert.equal(1, user.groups.filter(function(g){ return g.displayName === group; }).length, group); 87 | }); 88 | done(); 89 | }.bind(this)); 90 | }); 91 | 92 | it('should get user with groups by arbitrary property with type Edm.Guid', function (done) { 93 | var waad = new Waad({tenant: this.tenant, accessToken: this.accessToken}); 94 | waad.getUserByProperty('objectId', "9f7e9788-8081-4450-8d60-3b835aa2b54b", true, function(err, user) { 95 | if (err) return done(err); 96 | assert.notEqual(null, user); 97 | assert.equal(this.upn, user.userPrincipalName); 98 | assert.equal('Matias Woloski', user.displayName); 99 | assert.notEqual(undefined, user.groups); 100 | ['Test Group', 'Company Administrator'].forEach(function (group) { 101 | assert.equal(1, user.groups.filter(function(g){ return g.displayName === group; }).length, group); 102 | }); 103 | done(); 104 | }.bind(this)); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /lib/waad10.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var async = require('async'); 3 | 4 | module.exports = Waad10; 5 | 6 | function Waad10(options) { 7 | options = options || {}; 8 | this.options = options; 9 | 10 | if (!this.options.tenant) { 11 | throw new Error('Must supply "tenant" id (16a88858-..2d0263900406) or domain (mycompany.onmicrosoft.com)'); 12 | } 13 | 14 | if (!this.options.accessToken) { 15 | throw new Error('Must supply "accessToken"'); 16 | } 17 | 18 | if (this.options.fiddler) { 19 | request = request.defaults({'proxy':'http://127.0.0.1:8888'}); 20 | } 21 | } 22 | 23 | Waad10.prototype.__queryUserGroup = function (objectIdOrUpn, callback) { 24 | var headers = { 25 | 'Authorization': 'Bearer ' + this.options.accessToken, 26 | }; 27 | 28 | var qs = {}; 29 | qs['api-version'] = '1.0'; 30 | 31 | request({ 32 | url: 'https://graph.windows.net/' + this.options.tenant + '/users/' + objectIdOrUpn + '/memberOf', 33 | qs: qs, 34 | headers: headers 35 | }, function(err, resp, body) { 36 | if (err) return callback(err, null); 37 | 38 | if (resp.statusCode !== 200) { 39 | return callback(new Error(body), null); 40 | } 41 | 42 | var groups = JSON.parse(body); 43 | 44 | if (!groups && groups.value && groups.value.length === 0) 45 | return callback(); 46 | 47 | return callback(null, groups.value); 48 | }.bind(this)); 49 | }; 50 | 51 | Waad10.prototype.__queryUsers = function (qs, includeGroups, callback) { 52 | if (typeof includeGroups === 'function') { 53 | callback = includeGroups; 54 | includeGroups = false; 55 | } 56 | 57 | var headers = { 58 | 'Authorization': 'Bearer ' + this.options.accessToken, 59 | }; 60 | 61 | qs['api-version'] = '1.0'; 62 | 63 | request({ 64 | url: 'https://graph.windows.net/' + this.options.tenant + '/users', 65 | qs: qs, 66 | headers: headers 67 | }, function(err, resp, body) { 68 | if (err) return callback(err, null); 69 | if (resp.statusCode !== 200) { 70 | return callback(new Error(body), null); 71 | } 72 | 73 | var users = JSON.parse(body); 74 | 75 | if (!users && users.value && users.value.length === 0) 76 | return callback(); 77 | 78 | if (!includeGroups) 79 | return callback(null, users.value); 80 | 81 | async.forEach(users.value, function (user, cb) { 82 | this.__queryUserGroup(user.objectId, function (err, groups) { 83 | if (err) return callback(err); 84 | user.groups = groups; 85 | cb(); 86 | }); 87 | }.bind(this), function (err) { 88 | if (err) return callback(err); 89 | return callback(null, users.value); 90 | }); 91 | }.bind(this)); 92 | }; 93 | 94 | Waad10.prototype.getUserByEmail = function (email, callback) { 95 | var qs = { 96 | "$filter": "mail eq '" + email + "'", 97 | "$top" : 1 98 | }; 99 | 100 | return this.__queryUsers(qs, function (err, users) { 101 | if(err) return callback(err); 102 | return callback(null, users[0]); 103 | }); 104 | }; 105 | 106 | Waad10.prototype.getUserByUpn = function (upn, callback) { 107 | var qs = { 108 | "$filter": "userPrincipalName eq '" + upn + "'", 109 | "$top" : 1 110 | }; 111 | 112 | return this.__queryUsers(qs, function (err, users) { 113 | if(err) return callback(err); 114 | return callback(null, users[0]); 115 | }); 116 | }; 117 | 118 | Waad10.prototype.getUserByProperty = function (propertyName, propertyValue, includeGroups, callback) { 119 | if (typeof includeGroups === 'function') { 120 | callback = includeGroups; 121 | includeGroups = false; 122 | } 123 | 124 | var qs = { 125 | "$filter": propertyName + " eq '" + propertyValue + "'", 126 | "$top" : 1 127 | }; 128 | 129 | return this.__queryUsers(qs, includeGroups, function (err, users) { 130 | if (err) return callback(err); 131 | return callback(null, users[0]); 132 | }); 133 | }; 134 | 135 | Waad10.prototype.getUsers = function (options, callback) { 136 | var qs = {}; 137 | 138 | if(typeof options === 'object'){ 139 | if (options.top) { 140 | qs.$top = options.top; 141 | } 142 | 143 | if (options.skiptoken) { 144 | qs.$skiptoken = options.skiptoken; 145 | } 146 | } else { 147 | callback = options; 148 | } 149 | 150 | return this.__queryUsers(qs, callback); 151 | }; 152 | 153 | Waad10.prototype.getGroupsForUserByObjectIdOrUpn = function(objectIdOrUpn, callback) { 154 | return this.__queryUserGroup(objectIdOrUpn, callback); 155 | }; 156 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , jwt = require('jwt-simple') 3 | , moment = require('moment') 4 | , OAuthError = require('./errors').OAuthError 5 | , GraphClient = require('./waad') 6 | , GraphClient10 = require('./waad10'); 7 | 8 | var authenticator = module.exports = {}; 9 | 10 | authenticator.getAccessToken = function(tenantId, spnAppPrincipalId, spnSymmetricKeyBase64, callback) { 11 | var payload = { 12 | "aud": "00000001-0000-0000-c000-000000000000/accounts.accesscontrol.windows.net@" + tenantId, 13 | "iss": spnAppPrincipalId + "@" + tenantId, 14 | "nbf": moment().unix(), 15 | "exp": moment().add('hours', 1).unix() 16 | }; 17 | 18 | var key = new Buffer(spnSymmetricKeyBase64, 'base64').toString('binary'); 19 | var token = jwt.encode(payload, key); 20 | var data = { 21 | grant_type: 'http://oauth.net/grant_type/jwt/1.0/bearer', 22 | assertion: token, 23 | resource: '00000002-0000-0000-c000-000000000000/directory.windows.net@' + tenantId 24 | }; 25 | 26 | request.post('https://accounts.accesscontrol.windows.net/tokens/OAuth/2', { form: data }, function(e, resp, body) { 27 | if (e) return callback(e, null); 28 | 29 | if (resp.statusCode != 200) { 30 | try { 31 | var response = JSON.parse(body); 32 | if (response.error) { 33 | return callback(new OAuthError(response.error_description || response.error, response), null); 34 | } 35 | } 36 | catch(err) {} 37 | 38 | return callback(new OAuthError(body), null); 39 | } 40 | 41 | callback(null, JSON.parse(body).access_token); 42 | }); 43 | }; 44 | 45 | authenticator.getGraphClient = function (tenantId, spnAppPrincipalId, spnSymmetricKeyBase64, callback) { 46 | authenticator.getAccessToken(tenantId, spnAppPrincipalId, spnSymmetricKeyBase64, function (err, token) { 47 | if (err) return callback(err); 48 | callback(null, new GraphClient({tenant: tenantId, accessToken: token})); 49 | }); 50 | }; 51 | 52 | authenticator.getAccessTokenWithClientCredentials = function(tenantDomain, appDomain, clientId, clientSecret, callback) { 53 | var data = { 54 | grant_type: 'client_credentials', 55 | client_id: clientId + '/' + appDomain + '@' + tenantDomain, 56 | client_secret: clientSecret, 57 | resource: '00000002-0000-0000-c000-000000000000/graph.windows.net@' + tenantDomain 58 | }; 59 | 60 | request.post('https://accounts.accesscontrol.windows.net/' + tenantDomain + '/tokens/OAuth/2', { form: data }, function(e, resp, body) { 61 | if (e) return callback(e, null); 62 | 63 | if (resp.statusCode != 200) { 64 | try { 65 | var response = JSON.parse(body); 66 | if (response.error) { 67 | return callback(new OAuthError(response.error_description || response.error, response), null); 68 | } 69 | } 70 | catch (exp) {} 71 | 72 | return callback(new OAuthError(body), null); 73 | } 74 | 75 | callback(null, JSON.parse(body).access_token); 76 | }); 77 | }; 78 | 79 | authenticator.getAccessTokenWithClientCredentials2 = function(tenantDomain, clientId, clientSecret, callback) { 80 | var data = { 81 | grant_type: 'client_credentials', 82 | client_id: clientId, 83 | client_secret: clientSecret, 84 | resource: '00000002-0000-0000-c000-000000000000/graph.windows.net@' + tenantDomain 85 | }; 86 | 87 | request.post('https://login.windows.net/' + tenantDomain + '/oauth2/token', { form: data }, function(e, resp, body) { 88 | if (e) return callback(e, null); 89 | 90 | if (resp.statusCode != 200) { 91 | try { 92 | var response = JSON.parse(body); 93 | if (response.error) { 94 | return callback(new OAuthError(response.error_description || response.error, response), null); 95 | } 96 | } 97 | catch (exp) {} 98 | 99 | return callback(new OAuthError(body), null); 100 | } 101 | 102 | callback(null, JSON.parse(body).access_token); 103 | }); 104 | }; 105 | 106 | authenticator.getGraphClientWithClientCredentials = function(tenantDomain, appDomain, clientId, clientSecret, callback) { 107 | authenticator.getAccessTokenWithClientCredentials(tenantDomain, appDomain, clientId, clientSecret, function (err, token) { 108 | if(err) return callback (err); 109 | return callback(null, new GraphClient({tenant: tenantDomain, accessToken: token})); 110 | }); 111 | }; 112 | 113 | authenticator.getGraphClientWithClientCredentials2 = function(tenantDomain, clientId, clientSecret, callback) { 114 | authenticator.getAccessTokenWithClientCredentials2(tenantDomain, clientId, clientSecret, function (err, token) { 115 | if(err) return callback (err); 116 | return callback(null, new GraphClient({tenant: tenantDomain, accessToken: token})); 117 | }); 118 | }; 119 | 120 | authenticator.getGraphClient10 = function(tenantDomain, clientId, clientSecret, callback) { 121 | authenticator.getAccessTokenWithClientCredentials2(tenantDomain, clientId, clientSecret, function (err, token) { 122 | if(err) return callback (err); 123 | return callback(null, new GraphClient10({tenant: tenantDomain, accessToken: token})); 124 | }); 125 | }; 126 | 127 | 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Query Windows Azure Active Directory graph 2 | 3 | ``` 4 | npm install node-waad 5 | ``` 6 | 7 | ![](https://nodei.co/npm-dl/node-waad.png) 8 | 9 | ## General usage 10 | 11 | > UPDATE: we updated the package to use the api-version 1.0. We implemented a couple of methods only. The version 0.5 has more things implemented (like paging). 12 | 13 | 14 | Get an access token and query the graph 15 | 16 | Alternatively you can call ```waad.getGraphClient``` in one function 17 | 18 | ~~~javascript 19 | var waad = require('node-waad'); 20 | 21 | waad.getGraphClient10('auth10dev.onmicrosoft.com', 'client-id', 'client-secret', function(err, client) { 22 | // query the graph 23 | client.getUserByMail('matias@auth10dev.onmicrosoft.com', function(err, user) { 24 | // get user properties (user.displayName, user.mail, etc.) 25 | }); 26 | }); 27 | ~~~ 28 | 29 | ## Graph methods (API v1.0) 30 | 31 | ### getUsers([options], callback) 32 | 33 | Fetch a list of all users. 34 | 35 | **Callback** is a function with two arguments ```err``` and ```users```. Users is an array of user objects (paging not implemented, use v0.5) 36 | 37 | ### getUserByEmail(email, callback) 38 | 39 | Fetch one user by its email address. Parameters: 40 | 41 | - **email** the email address of the requested user. 42 | - **callback** is a function with two arguments ```err``` and ```user```. It will always return 1 user or null. 43 | 44 | ### getUserByProperty(propertyName, propertyValue, callback) 45 | 46 | Fetch one user by the specified property. Parameters: 47 | 48 | - **propertyName** the name of the property. 49 | - **propertyValue** the value of the property (match is exact). 50 | - **callback** is a function with two arguments ```err``` and ```user```. It will always return 1 user or null. 51 | 52 | ### getGroupsForUserByObjectIdOrUpn(objectIdOrUpn, callback) 53 | 54 | Fetch the list of groups the user belongs to. Parameters: 55 | 56 | - **objectIdOrUpn** the `objectId` or `userPrincipalName` of the user. 57 | - **callback** is a function with two arguments ```err``` and ```groups```. 58 | 59 | 60 | ## How to Get a client ID and client secret 61 | 62 | Read this tutorial from Microsoft 63 | [Adding, Updating, and Removing an App](http://msdn.microsoft.com/en-us/library/windowsazure/dn132599.aspx) 64 | 65 | ### API version 0.5 66 | 67 | Get an access token and query the graph 68 | 69 | Alternatively you can call ```waad.getGraphClient``` in one function 70 | 71 | ~~~javascript 72 | var waad = require('node-waad'); 73 | 74 | waad.getGraphClient('auth10dev.onmicrosoft.com', 'spn-appprincipal', 'symmetric-key-base64', function(err, client) { 75 | // query the graph 76 | client.getUserByEmail('matias@auth10dev.onmicrosoft.com', function(err, user) { 77 | // get user properties (user.DisplayName, user.Mail, etc.) 78 | }); 79 | }); 80 | ~~~ 81 | 82 | ``` 83 | 84 | Or use ```getGraphClientWithClientCredentials```: 85 | 86 | ```js 87 | var waad = require('node-waad'); 88 | 89 | waad.getGraphClientWithClientCredentials('auth10dev.onmicrosoft.com', 'myapp.com', 'client-id', 'client-secret', function(err, client) { 90 | // query the graph 91 | client.getUserByEmail('matias@auth10dev.onmicrosoft.com', function(err, user) { 92 | // get user properties (user.DisplayName, user.Mail, etc.) 93 | }); 94 | }); 95 | ``` 96 | 97 | ## Graph methods (API v0.5) 98 | 99 | ### getUsers([options], callback) 100 | 101 | Fetch a list of all users. 102 | 103 | **Callback** is a function with two arguments ```err``` and ```users```. Users is an array of user objects with few additional properties: 104 | 105 | - **hasMorePages** true if there are more users for this query 106 | - **skiptoken** if there is more pages for this query, you will have to use this skiptoken to get the next page. 107 | - **nextPage(callback)** if you want to fetch the next page inmediately, you can use this method instead of the afore mentioned skiptoken. The callback for this method works in the same way than the getUsers callback. 108 | 109 | **Options** has the following optional properties: 110 | 111 | - **includeGroups** optional (default ```false```) when set to true it will fetch the groups for each user and load them in the ```user.groups``` property. **Warning** when includeGroups is true an additional request will be made for every user. 112 | - **skiptoken** optional when set will fetch the next page of the result set. 113 | - **top** the maximum amount of users we want for this query. 114 | 115 | ### getUserByEmail(email, [includeGroups], callback) 116 | 117 | Fetch one user by its email address. Parameters: 118 | 119 | - **email** the email address of the requested user. 120 | - **includeGroups** optional (default ```false```) when set to true it will fetch the groups for the user and load them in the ```user.groups``` property. 121 | - **callback** is a function with two arguments ```err``` and ```user```. It will always return 1 user or null. 122 | 123 | ### getUserByProperty(accessToken, tenant, propertyName, propertyValue, [includeGroups], callback) 124 | 125 | Fetch one user by the specified property. Parameters: 126 | 127 | - **accessToken** a valid access token that you can obtain with the two afore mentioned methods. 128 | - **tenant** the id of the tenant. 129 | - **propertyName** the name of the property. 130 | - **propertyValue** the value of the property (match is exact). 131 | - **includeGroups** optional (default ```false```) when set to true it will fetch the groups for the user and load them in the ```user.groups``` property. 132 | - **callback** is a function with two arguments ```err``` and ```user```. It will always return 1 user or null. 133 | 134 | ### getGroupsForUserByEmail(email, callback) 135 | 136 | Fetch the list of groups the user belongs to. Parameters: 137 | 138 | - **email** the email address of the requested user. 139 | - **callback** is a function with two arguments ```err``` and ```groups```. 140 | 141 | ## License 142 | 143 | MIT - Auth0 144 | -------------------------------------------------------------------------------- /lib/waad.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | async = require('async'), 3 | url = require('url'), 4 | querystring = require('querystring'), 5 | xtend = require('xtend'); 6 | 7 | module.exports = Waad; 8 | 9 | function Waad(options) { 10 | options = options || {}; 11 | this.options = options; 12 | 13 | if (!this.options.tenant) { 14 | return callback(new Error('Must supply "tenant" id (16a88858-..2d0263900406) or domain (mycompany.onmicrosoft.com)'), null); 15 | } 16 | 17 | if (!this.options.accessToken) { 18 | return callback(new Error('Must supply "accessToken"'), null); 19 | } 20 | 21 | if (this.options.fiddler) { 22 | request = request.defaults({'proxy':'http://127.0.0.1:8888'}); 23 | } 24 | } 25 | 26 | Waad.prototype.__queryUserGroup = function (user, headers, callback) { 27 | request({ 28 | url: user.MemberOf.__deferred.uri, 29 | headers: headers 30 | }, function(err, resp, body) { 31 | if (err) return callback(err, null); 32 | 33 | if (resp.statusCode != 200) { 34 | return callback(new Error(body), null); 35 | } 36 | 37 | var groups = JSON.parse(body).d.results; 38 | return callback(null, groups); 39 | }); 40 | }; 41 | 42 | Waad.prototype.__queryUsers = function (qs, includeGroups, callback) { 43 | if (typeof(includeGroups) === 'function') { 44 | callback = includeGroups; 45 | includeGroups = false; 46 | } 47 | 48 | var headers = { 49 | 'Authorization': 'Bearer ' + this.options.accessToken, 50 | 'Accept': 'application/json;odata=verbose;charset=utf-8', 51 | 'x-ms-dirapi-data-contract-version': '0.5' 52 | }; 53 | 54 | request({ 55 | url: 'https://graph.windows.net/' + this.options.tenant + '/Users()', 56 | qs: qs, 57 | headers: headers 58 | }, function(err, resp, body) { 59 | if (err) return callback(err, null); 60 | 61 | if (resp.statusCode != 200) { 62 | return callback(new Error(body), null); 63 | } 64 | 65 | var d = JSON.parse(body).d, 66 | users = d.results, 67 | meta = buildMetadata(d); 68 | 69 | if (meta.skiptoken) { 70 | Object.defineProperty(users, 'nextPage', { 71 | enumerable: false, 72 | writeable: false, 73 | value: function (callback) { 74 | var newQs = xtend({}, qs); 75 | if (meta.skiptoken) { 76 | newQs.$skiptoken = meta.skiptoken; 77 | } else { 78 | delete newQs.$skiptoken; 79 | } 80 | this.__queryUsers(newQs, includeGroups, callback); 81 | }.bind(this) 82 | }); 83 | Object.defineProperty(users, 'skiptoken', { 84 | enumerable: false, 85 | writeable: false, 86 | value: meta.skiptoken 87 | }); 88 | } 89 | 90 | Object.defineProperty(users, 'hasMorePages', { 91 | enumerable: false, 92 | writeable: false, 93 | value: !!meta.skiptoken 94 | }); 95 | 96 | if (!users) 97 | return callback(null, null); 98 | 99 | if (!includeGroups) 100 | return callback(null, users, meta); 101 | 102 | async.forEach(users, function (user, callback) { 103 | this.__queryUserGroup(user, headers, function (err, groups) { 104 | if(err) return callback(err); 105 | user.groups = groups; 106 | callback(); 107 | }); 108 | }.bind(this), function (err) { 109 | if (err) return callback(err); 110 | return callback(null, users); 111 | }); 112 | }.bind(this)); 113 | }; 114 | 115 | function buildMetadata(d){ 116 | var result = {}; 117 | if (d.__next) { 118 | var parsedQueryString = querystring.parse(url.parse(d.__next).query), 119 | skiptoken = parsedQueryString.$skiptoken; 120 | result.skiptoken = skiptoken; 121 | } 122 | 123 | return result; 124 | } 125 | 126 | Waad.prototype.getUserByEmail = function (email, includeGroups, callback) { 127 | if (typeof(includeGroups) === 'function') { 128 | callback = includeGroups; 129 | includeGroups = false; 130 | } 131 | var qs = { 132 | "$filter": "Mail eq '" + email + "'", 133 | "$top" : 1 134 | }; 135 | return this.__queryUsers(qs, includeGroups, function (err, users) { 136 | if(err) return callback(err); 137 | return callback(null, users[0]); 138 | }); 139 | }; 140 | 141 | Waad.prototype.getUserByProperty = function (propertyName, propertyValue, include_groups, callback) { 142 | if (typeof(include_groups) === 'function') { 143 | callback = include_groups; 144 | include_groups = false; 145 | } 146 | var qs = { 147 | "$filter": propertyName + " eq '" + propertyValue + "'", 148 | "$top" : 1 149 | }; 150 | return this.__queryUsers(qs, include_groups, function (err, users) { 151 | if(err) return callback(err); 152 | return callback(null, users[0]); 153 | }); 154 | }; 155 | 156 | Waad.prototype.getUsers = function (options, callback) { 157 | var qs = {}; 158 | 159 | if(typeof options === 'object'){ 160 | if (options.top) { 161 | qs.$top = options.top; 162 | } 163 | if (options.skiptoken) { 164 | qs.$skiptoken = options.skiptoken; 165 | } 166 | } else { 167 | 168 | callback = options; 169 | 170 | } 171 | 172 | var includeGroups = options.includeGroups; 173 | 174 | if (options.all) { 175 | var users = []; 176 | var fetchPage = function (err, usrs) { 177 | if (err) return callback(err); 178 | users = users.concat(usrs); 179 | if (usrs.hasMorePages) { 180 | return usrs.nextPage(fetchPage); 181 | } 182 | callback(null, users); 183 | }; 184 | return this.__queryUsers(qs, includeGroups, fetchPage); 185 | } 186 | 187 | return this.__queryUsers(qs, includeGroups, callback); 188 | }; 189 | 190 | Waad.prototype.getGroupsForUserByEmail = function(email, callback) { 191 | this.getUserByEmail(email, true, function (err, user) { 192 | if (err) return callback(err); 193 | if (!user) return callback(null, null); 194 | return callback(null, user.groups); 195 | }); 196 | }; 197 | 198 | Waad.prototype.getGroupsForUserByProperty = function(propertyName, propertyValue, callback) { 199 | this.getUserByProperty(propertyName, propertyValue, true, function (err, user) { 200 | if (err) return callback(err); 201 | if (!user) return callback(null, null); 202 | return callback(null, user.groups); 203 | }); 204 | }; 205 | 206 | 207 | 208 | --------------------------------------------------------------------------------