Skip to content

Commit c955f80

Browse files
committed
Support LDAP authentication
1 parent a35ae85 commit c955f80

File tree

6 files changed

+260
-11
lines changed

6 files changed

+260
-11
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Below is a major feature comparison chart between the two drivers:
6666
| Password/PBKDF2 Authentication |:heavy_check_mark:|:heavy_check_mark:|
6767
| SAML Authentication |:heavy_check_mark:|:heavy_check_mark:|
6868
| JWT Authentication |:heavy_check_mark:|:heavy_check_mark:|
69-
| LDAP Authentication |:heavy_check_mark:|:x:|
69+
| LDAP Authentication |:heavy_check_mark:|:heavy_check_mark:|
7070
| Kerberos Authentication |:heavy_check_mark:|:x:|
7171
| X.509 Authentication |:heavy_check_mark:|:x:|
7272
| Secure User Store Integration (hdbuserstore) |:heavy_check_mark:|:x:|
@@ -195,7 +195,7 @@ This is suitable for multiple-host SAP HANA systems which are distributed over s
195195
Details about the different authentication methods can be found in the [SAP HANA Security Guide](https://help.sap.com/viewer/6b94445c94ae495c83a19646e7c3fd56/latest/en-US/440f6efe693d4b82ade2d8b182eb1efb.html).
196196

197197
#### User / Password
198-
Users authenticate themselves with their database `user` and `password`.
198+
Users authenticate themselves with their database `user` and `password`.
199199

200200
#### SAML assertion
201201
SAML bearer assertions as well as unsolicited SAML responses that include an

lib/protocol/auth/LDAP.js

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2022 SAP SE.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http: //www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing,
10+
// software distributed under the License is distributed on an
11+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12+
// either express or implied. See the License for the specific
13+
// language governing permissions and limitations under the License.
14+
'use strict';
15+
16+
var crypto = require('crypto');
17+
var util = require('../../util');
18+
var Fields = require('../data/Fields');
19+
20+
var CLIENT_NONCE_SIZE = 64;
21+
var CAPABILITIES_SIZE = 8;
22+
var DEFAULT_CAPABILITIES = 1;
23+
var SESSION_KEY_SIZE = 32; // AES256 key size
24+
25+
module.exports = LDAP;
26+
27+
/**
28+
* Handle LDAP authentication
29+
*
30+
* @param {object} options
31+
* @param {string|Buffer} options.password The LDAP password of the user
32+
* @param {Buffer} [options.clientChallenge] (for test only) the client nonce (64 bytes) to use
33+
* @param {Buffer} [options.sessionKey] (for test only) the AES256 key (32 bytes) for the encryption of the password
34+
*/
35+
function LDAP(options) {
36+
this.name = 'LDAP';
37+
this.password = options.password;
38+
if (util.isString(this.password)) {
39+
this.password = new Buffer(this.password, 'utf8');
40+
}
41+
this.clientNonce = options.clientChallenge || crypto.randomBytes(CLIENT_NONCE_SIZE);
42+
this.clientProof = null;
43+
this.sessionKey = options.sessionKey;
44+
}
45+
46+
/**
47+
* Return the initial data to send to HANA (client none + capabilities)
48+
* @return {Buffer}
49+
*/
50+
LDAP.prototype.initialData = function() {
51+
// prepare capabilities
52+
var capabilities = Buffer.allocUnsafe ? Buffer.allocUnsafe(CAPABILITIES_SIZE) : new Buffer(CAPABILITIES_SIZE);
53+
capabilities.writeInt8(DEFAULT_CAPABILITIES, 0);
54+
capabilities.fill(0, 1); // fill the remaining 7 bytes with 0
55+
56+
// write fields
57+
var data = Fields.write(null, [this.clientNonce, capabilities]).buffer;
58+
return data;
59+
};
60+
61+
/**
62+
* Gets the first response from the server and calculates the data for the next request
63+
* @param {Buffer} buffer
64+
* @param {function(Error?)} cb
65+
*/
66+
LDAP.prototype.initialize = function(buffer, cb) {
67+
// read server challenge
68+
var serverChallengeData = Fields.read({
69+
buffer: buffer
70+
});
71+
72+
// check number of fields
73+
if (serverChallengeData.length < 4) {
74+
var error = new Error('Unexpected number of fields [' + serverChallengeData.length + '] in server challenge (LDAP authentication)');
75+
error.code = 'EHDBAUTHPROTOCOL';
76+
cb(error);
77+
return;
78+
}
79+
80+
// check client nonce
81+
var clientNonceProof = serverChallengeData[0];
82+
if (!clientNonceProof.equals(this.clientNonce)) {
83+
var error = new Error('Client nonce does not match (LDAP authentication)');
84+
error.code = 'EHDBAUTHCLIENTNONCE';
85+
cb(error);
86+
return;
87+
}
88+
89+
// check capabilities
90+
var serverCapabilities = serverChallengeData[3];
91+
if (serverCapabilities.readInt8() != DEFAULT_CAPABILITIES) {
92+
var error = new Error('Unsupported capabilities (LDAP authentication)');
93+
error.code = 'EHDBAUTHCAPABILITIES';
94+
cb(error);
95+
return;
96+
}
97+
98+
// generate session key (for AES256 encryption of the password)
99+
if (!this.sessionKey) {
100+
this.sessionKey = crypto.randomBytes(SESSION_KEY_SIZE);
101+
}
102+
103+
// generate the encrypted session key
104+
var serverNonce = serverChallengeData[1];
105+
var serverPublicKey = serverChallengeData[2].toString('ascii'); // RSA public key (PKCS8 PEM)
106+
var sessionKeyContent = Buffer.concat([this.sessionKey, serverNonce]);
107+
var encryptedSessionKey = crypto.publicEncrypt({
108+
key: serverPublicKey,
109+
format: 'pem',
110+
type: 'spki'
111+
}, sessionKeyContent);
112+
113+
// encrypt the password
114+
var iv = serverNonce.slice(0, 16);
115+
var cipher = crypto.createCipheriv("aes-256-cbc", this.sessionKey, iv);
116+
var passwordContent = Buffer.concat([this.password, new Buffer(1), serverNonce]);
117+
var encryptedPassword = cipher.update(passwordContent);
118+
encryptedPassword = Buffer.concat([encryptedPassword, cipher.final()]);
119+
120+
// generate client proof
121+
this.clientProof = Fields.write(null, [encryptedSessionKey, encryptedPassword]).buffer;
122+
123+
// done
124+
cb();
125+
};
126+
127+
LDAP.prototype.finalData = function finalData() {
128+
return this.clientProof;
129+
};
130+
131+
LDAP.prototype.finalize = function finalize(buffer) {
132+
/* jshint unused:false */
133+
};

lib/protocol/auth/Manager.js

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var SCRAMSHA256 = require('./SCRAMSHA256');
1717
var SAML = require('./SAML');
1818
var JWT = require('./JWT');
1919
var SessionCookie = require('./SessionCookie');
20+
var LDAP = require('./LDAP');
2021

2122
module.exports = Manager;
2223

@@ -35,6 +36,7 @@ function Manager(options) {
3536
this._authMethods.push(new SessionCookie(options));
3637
}
3738
if (options.user && options.password) {
39+
this._authMethods.push(new LDAP(options));
3840
this._authMethods.push(new SCRAMSHA256(options, true)); // with PBKDF2
3941
this._authMethods.push(new SCRAMSHA256(options, false)); // no PBKDF2
4042
}

lib/protocol/data/Fields.js

+3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ function read(part) {
4444
} else if (fieldLength == common.DATA_LENGTH_4BYTE_LENGTH_INDICATOR) {
4545
fieldLength = buffer.readUInt32LE(offset);
4646
offset += 4;
47+
} else if (fieldLength == 255) { // indicates that the following two bytes contain the length in big endian (bug?)
48+
fieldLength = buffer.readUInt16BE(offset);
49+
offset += 2;
4750
} else {
4851
throw Error("Unsupported length indicator: " + fieldLength);
4952
}

test/auth.Manager.js

+118-7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('Auth', function () {
2626

2727
describe('#SCRAMSHA256', function () {
2828

29+
var method0 = 'LDAP'
2930
var method1 = 'SCRAMPBKDF2SHA256';
3031
var method2 = 'SCRAMSHA256';
3132
var password = 'secret';
@@ -56,12 +57,69 @@ describe('Auth', function () {
5657
'01002093cae8d0d3fd9ea7e67da4a09678d504429e67a1cb6197ed3a6a70afbd757a96',
5758
'hex');
5859

60+
var ldapClientChallenge = new Buffer(
61+
'0200' +
62+
// client nonce = clientChallenge
63+
'40' +
64+
'edbd7cc8b2f26489d65a7cd51e27f2e73fca227d1ab6aafcac0f428ca4d8e10c' +
65+
'19e3e38f3aac51075e67bbe52fdb6103a7c34c8a70908ed5be0b3542705f738c' +
66+
// supported capabilities
67+
'08' +
68+
'0100000000000000',
69+
'hex');
70+
var ldapServerChallenge = new Buffer(
71+
'0400' +
72+
// client nonce = clientChallenge
73+
'40' +
74+
'edbd7cc8b2f26489d65a7cd51e27f2e73fca227d1ab6aafcac0f428ca4d8e10c' +
75+
'19e3e38f3aac51075e67bbe52fdb6103a7c34c8a70908ed5be0b3542705f738c' +
76+
// server nonce
77+
'40' +
78+
'a16fc718d5fd20aa3febeeeebe34270565ad3818894c6e3b3b674ee71b440c07' +
79+
'd6b9329d1860d4e693d9312aaece14bf3eb86d604670c571f2d7445a97949310' +
80+
// public key pem
81+
'ff01c4' +
82+
'2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a' +
83+
'4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b43415145416f4e736d494777763554583558473051697076620a' +
84+
'435342576645773678546d3230596c555559516262316d5863764831575153475877424c5078313449556b584e67545350435177314a4f7361513075364843680a' +
85+
'6e35773063786f4e78367a386e694b3838676c774a476167714c32356536506d47354d586264784d74496c5863736336465a55364a4370384538496d313362650a' +
86+
'7776584c4b6c6d7536304238762b462b5877582b5a6b6f693735662f6758626e2f366a723679737a554c4b512f586151524a69535766567468575a71533967540a' +
87+
'53645676686e736d4e306261744c7a70705376706c79356447423735596961754b4f66672b753531684e2b4b4d4a5a532f392f415172716d71637678675835740a' +
88+
'79624a6b6138796e437164694e4b6d32764d6174766d6f656a4f446d7a61474b5553514754627042357a35654a544636625172796877666850645263692b7a760a' +
89+
'7a514944415141420a' +
90+
'2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a00' +
91+
// capability to use
92+
'01' +
93+
'01',
94+
'hex');
95+
/*
96+
-----BEGIN PUBLIC KEY-----
97+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoNsmIGwv5TX5XG0Qipvb
98+
CSBWfEw6xTm20YlUUYQbb1mXcvH1WQSGXwBLPx14IUkXNgTSPCQw1JOsaQ0u6HCh
99+
n5w0cxoNx6z8niK88glwJGagqL25e6PmG5MXbdxMtIlXcsc6FZU6JCp8E8Im13be
100+
wvXLKlmu60B8v+F+XwX+Zkoi75f/gXbn/6jr6yszULKQ/XaQRJiSWfVthWZqS9gT
101+
SdVvhnsmN0batLzppSvply5dGB75YiauKOfg+u51hN+KMJZS/9/AQrqmqcvxgX5t
102+
ybJka8ynCqdiNKm2vMatvmoejODmzaGKUSQGTbpB5z5eJTF6bQryhwfhPdRci+zv
103+
zQIDAQAB
104+
-----END PUBLIC KEY-----
105+
*/
106+
var ldapSessionKey = new Buffer(
107+
'568bdf7b9d8930ea937279326c92f72fc0769205e91d864b7a44868984e2cbb2',
108+
'hex');
109+
// can't check client proof as RSA encypt has a random factor
110+
// so only check the encrypted password
111+
var ldapEncryptedPassword = new Buffer('7d78f690a5cff122e72f62f6c07dfea1259098a2ccfa938f42031f2644dba8954aa10d1c0665d7b8ac763353acfd0792cea2a6dea423c85c62efb8448f398bc9425aee548b1cdb22cbb4d6d3a95eaf70', 'hex');
112+
59113
it('should get the corresponding authentication method instances', function () {
60114
var manager = auth.createManager({
61115
user: user,
62116
password: new Buffer(password, 'utf8'),
63117
clientChallenge: clientChallenge
64118
});
119+
var authMethod0 = manager.getMethod(method0);
120+
Buffer.isBuffer(authMethod0.password).should.be.true;
121+
authMethod0.password.toString('utf8').should.equal(password);
122+
65123
var authMethod1 = manager.getMethod(method1);
66124
Buffer.isBuffer(authMethod1.password).should.be.true;
67125
authMethod1.password.toString('utf8').should.equal(password);
@@ -78,8 +136,8 @@ describe('Auth', function () {
78136
clientChallenge: clientChallenge
79137
});
80138
manager.user.should.equal(user);
81-
manager._authMethods.should.have.length(2);
82-
var authMethod = manager._authMethods[1];
139+
manager._authMethods.should.have.length(3);
140+
var authMethod = manager._authMethods[2];
83141
authMethod.name.should.equal(method2);
84142
authMethod.password.should.be.instanceof(Buffer);
85143
authMethod.password.toString('utf8').should.eql(password);
@@ -88,7 +146,7 @@ describe('Auth', function () {
88146
var initialData = authMethod.initialData();
89147
initialData.should.equal(clientChallenge);
90148
initialData = manager.initialData();
91-
initialData.should.eql([user, method1, clientChallenge, method2, clientChallenge]);
149+
initialData.should.eql([user, method0, ldapClientChallenge, method1, clientChallenge, method2, clientChallenge]);
92150
// initialize manager
93151
manager.initialize([method2, serverChallengeDataNoPBKDF2], function(err) {
94152
manager._authMethod.should.equal(authMethod);
@@ -112,8 +170,8 @@ describe('Auth', function () {
112170
clientChallenge: clientChallenge
113171
});
114172
manager.user.should.equal(user);
115-
manager._authMethods.should.have.length(2);
116-
var authMethod = manager._authMethods[0];
173+
manager._authMethods.should.have.length(3);
174+
var authMethod = manager._authMethods[1];
117175
authMethod.name.should.equal(method1);
118176
authMethod.password.should.be.instanceof(Buffer);
119177
authMethod.password.toString('utf8').should.eql(password);
@@ -122,7 +180,7 @@ describe('Auth', function () {
122180
var initialData = authMethod.initialData();
123181
initialData.should.equal(clientChallenge);
124182
initialData = manager.initialData();
125-
initialData.should.eql([user, method1, clientChallenge, method2, clientChallenge]);
183+
initialData.should.eql([user, method0, ldapClientChallenge, method1, clientChallenge, method2, clientChallenge]);
126184
// initialize manager
127185
manager.initialize([method1, serverChallengeDataWithPBKDF2], function(err) {
128186
manager._authMethod.should.equal(authMethod);
@@ -146,6 +204,45 @@ describe('Auth', function () {
146204
});
147205
});
148206

207+
it('should authenticate and connect successfully with LDAP', function (done) {
208+
var manager = auth.createManager({
209+
user: user,
210+
password: password,
211+
clientChallenge: clientChallenge,
212+
sessionKey: ldapSessionKey
213+
});
214+
manager.user.should.equal(user);
215+
manager._authMethods.should.have.length(3);
216+
var authMethod = manager._authMethods[0];
217+
authMethod.name.should.equal(method0);
218+
authMethod.password.should.be.instanceof(Buffer);
219+
authMethod.password.toString('utf8').should.eql(password);
220+
authMethod.clientNonce.should.equal(clientChallenge);
221+
authMethod.sessionKey.should.equal(ldapSessionKey);
222+
// initial data
223+
var initialData = authMethod.initialData();
224+
initialData.should.eql(ldapClientChallenge);
225+
initialData = manager.initialData();
226+
initialData.should.eql([user, method0, ldapClientChallenge, method1, clientChallenge, method2, clientChallenge]);
227+
// initialize manager
228+
manager.initialize([method0, ldapServerChallenge], function(err) {
229+
manager._authMethod.should.equal(authMethod);
230+
// clientProof
231+
var ldapClientProof = authMethod.clientProof;
232+
var clientProofFields = Fields.read({ buffer: ldapClientProof });
233+
clientProofFields.length.should.eql(2);
234+
clientProofFields[1].should.eql(ldapEncryptedPassword);
235+
// final data
236+
var finalData = authMethod.finalData();
237+
finalData.should.eql(ldapClientProof);
238+
finalData = manager.finalData();
239+
finalData.should.eql([user, method0, ldapClientProof]);
240+
// finalize manager
241+
manager.finalize([method0, null]);
242+
done();
243+
});
244+
});
245+
149246
it('should write initial data fields part', function () {
150247
var part = Fields.write({}, auth.createManager({
151248
user: user,
@@ -156,7 +253,7 @@ describe('Auth', function () {
156253
var buffer = part.buffer;
157254
var offset = 0;
158255
var field, length;
159-
buffer.readUInt16LE(offset).should.equal(5);
256+
buffer.readUInt16LE(offset).should.equal(7);
160257
offset += 2;
161258
// validate user
162259
length = buffer[offset];
@@ -165,6 +262,20 @@ describe('Auth', function () {
165262
offset += length;
166263
length.should.equal(Buffer.byteLength(user));
167264
field.should.equal(user);
265+
// validate method0 name
266+
length = buffer[offset];
267+
offset += 1;
268+
field = buffer.toString('utf8', offset, offset + length);
269+
offset += length;
270+
length.should.equal(Buffer.byteLength(method0));
271+
field.should.equal(method0);
272+
// validate clientChallenge #0
273+
length = buffer[offset];
274+
offset += 1;
275+
field = buffer.slice(offset, offset + length);
276+
offset += length;
277+
length.should.equal(ldapClientChallenge.length);
278+
field.should.eql(ldapClientChallenge);
168279
// validate method1 name
169280
length = buffer[offset];
170281
offset += 1;

test/mock/server.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ function handleAuthenticate(msg) {
146146
var fields = Fields.read(msgPart);
147147
var user = fields[0];
148148
var algorithm = fields[1].toString('ascii');
149-
if (algorithm === "SCRAMPBKDF2SHA256") {
150-
var algorithm = fields[3].toString('ascii');
149+
if (algorithm === "LDAP") {
150+
var algorithm = fields[5].toString('ascii');
151151
}
152152
var salt = new Buffer([
153153
0x80, 0x96, 0x4f, 0xa8, 0x54, 0x28, 0xae, 0x3a,

0 commit comments

Comments
 (0)