Skip to content

Commit 6c97b00

Browse files
committed
Add LDAP auth module
1 parent 5cfaaf0 commit 6c97b00

File tree

5 files changed

+225
-0
lines changed

5 files changed

+225
-0
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"graphql-upload": "8.1.0",
3939
"intersect": "1.0.1",
4040
"jsonwebtoken": "8.5.1",
41+
"ldapjs": "^1.0.2",
4142
"lodash": "4.17.15",
4243
"lru-cache": "5.1.1",
4344
"mime": "2.4.4",

spec/LdapAuth.spec.js

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const ldap = require('../lib/Adapters/Auth/ldap');
2+
const mockLdapServer = require('./MockLdapServer');
3+
4+
it('Should fail with missing options', done => {
5+
try {
6+
ldap.validateAuthData({ id: 'testuser', password: 'testpw' });
7+
} catch (error) {
8+
jequal(error.message, 'LDAP auth configuration missing');
9+
done();
10+
}
11+
});
12+
13+
it('Should succeed with right credentials', done => {
14+
mockLdapServer(1010, 'uid=testuser, o=example').then(server => {
15+
const options = {
16+
suffix: 'o=example',
17+
url: 'ldap://localhost:1010',
18+
dn: 'uid={{id}}, o=example',
19+
};
20+
ldap
21+
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
22+
.then(done)
23+
.catch(done.fail)
24+
.finally(() => server.close());
25+
});
26+
});
27+
28+
it('Should fail with wrong credentials', done => {
29+
mockLdapServer(1010, 'uid=testuser, o=example').then(server => {
30+
const options = {
31+
suffix: 'o=example',
32+
url: 'ldap://localhost:1010',
33+
dn: 'uid={{id}}, o=example',
34+
};
35+
ldap
36+
.validateAuthData({ id: 'testuser', password: 'wrong!' }, options)
37+
.then(done.fail)
38+
.catch(err => {
39+
jequal(err.message, 'LDAP: Wrong username or password');
40+
done();
41+
})
42+
.finally(() => server.close());
43+
});
44+
});
45+
46+
it('Should succeed if user is in given group', done => {
47+
mockLdapServer(1010, 'uid=testuser, o=example').then(server => {
48+
const options = {
49+
suffix: 'o=example',
50+
url: 'ldap://localhost:1010',
51+
dn: 'uid={{id}}, o=example',
52+
groupCn: 'powerusers',
53+
groupFilter:
54+
'(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
55+
};
56+
57+
ldap
58+
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
59+
.then(done)
60+
.catch(done.fail)
61+
.finally(() => server.close());
62+
});
63+
});
64+
65+
it('Should fail if user is not in given group', done => {
66+
mockLdapServer(1010, 'uid=testuser, o=example').then(server => {
67+
const options = {
68+
suffix: 'o=example',
69+
url: 'ldap://localhost:1010',
70+
dn: 'uid={{id}}, o=example',
71+
groupDn: 'ou=somegroup, o=example',
72+
groupFilter:
73+
'(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
74+
};
75+
76+
ldap
77+
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
78+
.then(done.fail)
79+
.catch(err => {
80+
jequal(err.message, 'LDAP: User not in group');
81+
done();
82+
})
83+
.finally(() => server.close());
84+
});
85+
});

spec/MockLdapServer.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const ldapjs = require('ldapjs');
2+
3+
function newServer(port, dn) {
4+
const server = ldapjs.createServer();
5+
6+
server.bind('o=example', function(req, res, next) {
7+
if (req.dn.toString() !== dn || req.credentials !== 'secret')
8+
return next(new ldapjs.InvalidCredentialsError());
9+
res.end();
10+
return next();
11+
});
12+
13+
server.search('o=example', function(req, res) {
14+
const obj = {
15+
dn: req.dn.toString(),
16+
attributes: {
17+
objectclass: ['organization', 'top'],
18+
o: 'example',
19+
},
20+
};
21+
22+
const group = {
23+
dn: req.dn.toString(),
24+
attributes: {
25+
objectClass: ['groupOfUniqueNames', 'top'],
26+
uniqueMember: ['uid=testuser, o=example'],
27+
cn: 'powerusers',
28+
ou: 'powerusers',
29+
},
30+
};
31+
32+
if (req.filter.matches(obj.attributes)) {
33+
res.send(obj);
34+
}
35+
36+
if (req.filter.matches(group.attributes)) {
37+
res.send(group);
38+
}
39+
res.end();
40+
});
41+
return new Promise(resolve => server.listen(port, () => resolve(server)));
42+
}
43+
44+
module.exports = newServer;

src/Adapters/Auth/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const weibo = require('./weibo');
2323
const oauth2 = require('./oauth2');
2424
const phantauth = require('./phantauth');
2525
const microsoft = require('./microsoft');
26+
const ldap = require('./ldap');
2627

2728
const anonymous = {
2829
validateAuthData: () => {
@@ -57,6 +58,7 @@ const providers = {
5758
weibo,
5859
phantauth,
5960
microsoft,
61+
ldap,
6062
};
6163

6264
function authDataValidator(adapter, appIds, options) {

src/Adapters/Auth/ldap.js

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
const ldapjs = require('ldapjs');
2+
const Parse = require('parse/node').Parse;
3+
4+
function validateAuthData(authData, options) {
5+
if (!optionsAreValid(options)) {
6+
throw new Parse.Error(
7+
Parse.Error.INTERNAL_SERVER_ERROR,
8+
'LDAP auth configuration missing'
9+
);
10+
}
11+
12+
const client = ldapjs.createClient({ url: options.url });
13+
const userCn =
14+
typeof options.dn === 'string'
15+
? options.dn.replace('{{id}}', authData.id)
16+
: `uid=${authData.id},${options.suffix}`;
17+
18+
return new Promise((resolve, reject) => {
19+
client.bind(userCn, authData.password, err => {
20+
if (err) {
21+
client.destroy(err);
22+
return reject(
23+
new Parse.Error(
24+
Parse.Error.OBJECT_NOT_FOUND,
25+
'LDAP: Wrong username or password'
26+
)
27+
);
28+
}
29+
30+
if (
31+
typeof options.groupCn === 'string' &&
32+
typeof options.groupFilter === 'string'
33+
) {
34+
const filter = options.groupFilter.replace(/{{id}}/gi, authData.id);
35+
const opts = {
36+
scope: 'sub',
37+
filter: filter,
38+
};
39+
let found = false;
40+
client.search(options.suffix, opts, (searchError, res) => {
41+
if (searchError) {
42+
throw new Parse.Error(
43+
Parse.Error.INTERNAL_SERVER_ERROR,
44+
'LDAP group search failed'
45+
);
46+
}
47+
res.on('searchEntry', function(entry) {
48+
if (entry.object.cn === options.groupCn) {
49+
found = true;
50+
client.unbind();
51+
client.destroy();
52+
return resolve();
53+
}
54+
});
55+
res.on('end', function() {
56+
if (!found) {
57+
client.unbind();
58+
client.destroy();
59+
reject(
60+
new Parse.Error(
61+
Parse.Error.INTERNAL_SERVER_ERROR,
62+
'LDAP: User not in group'
63+
)
64+
);
65+
}
66+
});
67+
});
68+
} else {
69+
client.unbind();
70+
client.destroy();
71+
resolve();
72+
}
73+
});
74+
});
75+
}
76+
77+
function optionsAreValid(options) {
78+
return (
79+
typeof options === 'object' &&
80+
typeof options.suffix === 'string' &&
81+
typeof options.url === 'string' &&
82+
options.url.startsWith('ldap://')
83+
);
84+
}
85+
86+
function validateAppId() {
87+
return Promise.resolve();
88+
}
89+
90+
module.exports = {
91+
validateAppId,
92+
validateAuthData,
93+
};

0 commit comments

Comments
 (0)