Skip to content
This repository has been archived by the owner on May 14, 2024. It is now read-only.

Support sasl authentication #826

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ You probably won't need to check the `res` parameter, but it's there if you do.

Performs a bind operation against the LDAP server.

The bind API only allows LDAP 'simple' binds (equivalent to HTTP Basic
Authentication) for now. Note that all client APIs can optionally take an array
The bind method allows LDAP 'simple' binds (equivalent to HTTP Basic
Authentication). Note that all client APIs can optionally take an array
of `Control` objects. You probably don't need them though...

Example:
Expand All @@ -131,6 +131,30 @@ client.bind('cn=root', 'secret', (err) => {
});
```

# saslBind
`saslBind(token, controls, callback)`

Performs a sasl bind operation against the LDAP server.

Allows you to authenticate using a challange-response mechanism (NTLM/GSSAPI).
This methods accepts a type 1 or type 3 token as a string or Buffer.
Note that all client APIs can optionally take an array
of `Control` objects. You probably don't need them though...

Example:

```js
const match = authorizationHeader.match(/^NTLM\s(.+)$/);
const authBase64 = match ? match[1] : '';
const token = Buffer.from(authBase64, 'base64');
client.saslBind(token, (err, res) => {
if (res && res.saslChallange) {
console.log(res.saslChallange);
}
assert.ifError(err);
});
```

# add
`add(dn, entry, controls, callback)`

Expand Down
40 changes: 40 additions & 0 deletions lib/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,46 @@ Client.prototype.bind = function bind (name,
return this._send(req, [errors.LDAP_SUCCESS], null, callbackWrapper, _bypass)
}

/**
* Performs a sasl (NTLM/GSSAPI) authentication against the server.
*
* @param {String} token the token from the authentication http header.
* @param {Control} controls (optional) either a Control or [Control].
* @param {Function} callback of the form f(err, res).
* @throws {TypeError} on invalid input.
*/
Client.prototype.saslBind = function saslBind (token,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to add a corresponding test for this in `client.test.js?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

controls,
callback,
_bypass) {
if (typeof (token) !== 'string' && !Buffer.isBuffer(token)) {
throw new TypeError('token (string or Buffer) required')
}
if (typeof (controls) === 'function') {
callback = controls
controls = []
} else {
controls = validateControls(controls)
}
Comment on lines +309 to +321
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather the function accept an input object so that positional parameter inspection is unnecessary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you talking about an incopatible change also for client.bind()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. This is a completely new method. There is no reason to carry forward difficult to maintain patterns.

assert.func(callback, 'callback')

const req = new BindRequest({
credentials: token || '',
authentication: 'sasl',
controls: controls
})

// Connection errors will be reported to the bind callback too (useful when the LDAP server is not available)
const self = this
function callbackWrapper (err, ret) {
self.removeListener('connectError', callbackWrapper)
callback(err, ret)
}
this.addListener('connectError', callbackWrapper)

return this._send(req, [errors.LDAP_SUCCESS, errors.LDAP_SASL_BIND_IN_PROGRESS], null, callbackWrapper, _bypass)
}

/**
* Compares an attribute/value pair with an entry on the LDAP server.
*
Expand Down
163 changes: 155 additions & 8 deletions lib/messages/bind_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ const Protocol = require('../protocol')

const Ber = asn1.Ber
const LDAP_BIND_SIMPLE = 'simple'
// var LDAP_BIND_SASL = 'sasl'
const LDAP_BIND_SASL = 'sasl'
const SASL_SEQUENCE = 0xa3
const SASL_SEQUENCE_TYPE1 = 0x60
const SASL_SEQUENCE_TYPE3 = 0xa1
const SASL_TOKENLEN_TYPE1 = 40
const SASL_TOKENLEN_TYPE3 = 522

/// --- API

Expand Down Expand Up @@ -48,22 +53,164 @@ BindRequest.prototype._parse = function (ber) {

const t = ber.peek()

// TODO add support for SASL et al
if (t !== Ber.Context) { throw new Error('authentication 0x' + t.toString(16) + ' not supported') }

this.authentication = LDAP_BIND_SIMPLE
this.credentials = ber.readString(Ber.Context)
if (t === SASL_SEQUENCE) {
this.authentication = LDAP_BIND_SASL
ber.readSequence(SASL_SEQUENCE)
this.ldapMechanism = ber.readString(Ber.OctetString)
ber.readSequence(0x04) // ldapCredentials

const typeSequence = ber.readSequence() // GSSAPI
if (typeSequence === SASL_SEQUENCE_TYPE1) {
this.gssapiOID = ber.readOID(Ber.OID)
ber.readSequence(0xa0) // SPNEGO
ber.readSequence(0x30)
ber.readSequence(0xa0)
ber.readSequence(0x30)
this.spnegoMechType = ber.readOID(Ber.OID)
ber.readSequence(0xa2)
this.credentials = ber.readString(Ber.OctetString, true)
} else if (typeSequence === SASL_SEQUENCE_TYPE3) {
ber.readSequence(0x30) // spnego.negTokenTarg
ber.readSequence(0xa2)
ber.readSequence(0x04)
this.credentials = ber.buffer
this.ntlmsspIdentifier = readByteString(ber, 8)
this.ntlmsspMessageType = read32bitNumber(ber)
this.ntlmsspLanManager = extractDataFromToken(this.credentials, ber)
this.ntlmsspNtlm = extractDataFromToken(this.credentials, ber)
this.ntlmsspDomain = extractDataFromToken(this.credentials, ber)
this.ntlmsspUser = extractDataFromToken(this.credentials, ber)
this.ntlmsspHost = extractDataFromToken(this.credentials, ber)
this.ntlmsspAuthSessionKey = readByteString(ber, 8)
const flag4 = ber.readByte()
const flag3 = ber.readByte()
const flag2 = ber.readByte()
const flag1 = ber.readByte()
this.ntlmsspNegotiateFlags = {
Negotiate56: !!(flag1 & 128),
NegotiateKeyExchange: !!(flag1 & 64),
Negotiate128: !!(flag1 & 32),
Negotiate0x10000000: !!(flag1 & 16),
Negotiate0x08000000: !!(flag1 & 8),
Negotiate0x04000000: !!(flag1 & 4),
NegotiateVersion: !!(flag1 & 2),
Negotiate0x01000000: !!(flag1 & 1),
NegotiateTargetInfo: !!(flag2 & 128),
RequestNonNtSession: !!(flag2 & 64),
Negotiate0x00200000: !!(flag2 & 32),
NegotiateIdentify: !!(flag2 & 16),
NegotiateExtendesSecurity: !!(flag2 & 8),
TargetTypeShare: !!(flag2 & 4),
TargetTypeServer: !!(flag2 & 2),
TargetTypeDomain: !!(flag2 & 1),
NegotiateAlwaysSign: !!(flag3 & 128),
Negotiate0x00004000: !!(flag3 & 64),
NegotiateOemWorkstationSupplied: !!(flag3 & 32),
NegotiateOemDomainSupplied: !!(flag3 & 16),
NegotiateAnonymous: !!(flag3 & 8),
NegotiateNtOnly: !!(flag3 & 4),
NegotiateNtlmKey: !!(flag3 & 2),
Negotiate0x00000100: !!(flag3 & 1),
NegotiateLanManagerKey: !!(flag4 & 128),
NegotiateDatagram: !!(flag4 & 64),
NegotiateSeal: !!(flag4 & 32),
NegotiateSign: !!(flag4 & 16),
Request0x00000008: !!(flag4 & 8),
RequestTarget: !!(flag4 & 4),
TargetOem: !!(flag4 & 2),
TargetUnicode: !!(flag4 & 1)
}
this.ntlmsspVersionMajor = ber.readByte()
this.ntlmsspVersionMinor = ber.readByte()
this.ntlmsspBuildNumber = read16bitNumber(ber)
ber.readByte(); ber.readByte(); ber.readByte()
this.ntlmsspNtlmCurrentRev = ber.readByte()
this.messageIntegrityCode = readByteString(ber, 16)
} else {
console.error('SASL sequence %s found, expected %s (for type1) or %s (for type3) found',
typeSequence.toString(16), SASL_SEQUENCE_TYPE1.toString(16), SASL_SEQUENCE_TYPE3.toString(16))
}
} else if (t === Ber.Context) {
this.authentication = LDAP_BIND_SIMPLE
this.credentials = ber.readString(Ber.Context)
} else {
throw new Error('authentication 0x' + t.toString(16) + ' not supported')
}

return true
}

function extractDataFromToken (buf, ber) {
const length = read16bitNumber(ber)
read16bitNumber(ber) // maxlen
const offset = read32bitNumber(ber)
return buf.slice(offset, offset + length).toString('utf16le')
}
function read16bitNumber (ber) { return ber.readByte() + (ber.readByte() << 8) }
function read32bitNumber (ber) { return ber.readByte() + (ber.readByte() << 8) + (ber.readByte() << 16) + (ber.readByte() << 24) }
function readByteString (ber, len) { return String.fromCharCode(...range(0, len - 1).map(function () { return ber.readByte() })) }
function range (start, end) {
return Array.from({ length: end - start + 1 }, (_, i) => i)
}

BindRequest.prototype._toBer = function (ber) {
assert.ok(ber)

ber.writeInt(this.version)
ber.writeString((this.name || '').toString())
// TODO add support for SASL et al
ber.writeString((this.credentials || ''), Ber.Context)

if (this.authentication === 'sasl') {
ber.startSequence(SASL_SEQUENCE)
ber.writeString('GSS-SPNEGO', Ber.OctetString)
ber.startSequence(0x04)

if (this.credentials.length === SASL_TOKENLEN_TYPE1) {
ber.startSequence(SASL_SEQUENCE_TYPE1)
ber.writeOID('1.3.6.1.5.5.2', Ber.OID) // gssapiOID
ber.startSequence(0xa0) // SPNEGO
ber.startSequence(0x30)
ber.startSequence(0xa0)
ber.startSequence(0x30)
ber.writeOID('1.3.6.1.4.1.311.2.2.10', Ber.OID) // spnegoMechType
ber.endSequence()
ber.endSequence()
ber.startSequence(0xa2)

// spnegoMechToken
if (Buffer.isBuffer(this.credentials)) {
ber.writeBuffer(this.credentials, Ber.OctetString)
} else {
ber.writeString(this.credentials, Ber.OctetString)
}

ber.endSequence()
ber.endSequence()
ber.endSequence()
ber.endSequence()
} else if (this.credentials.length === SASL_TOKENLEN_TYPE3) {
ber.startSequence(SASL_SEQUENCE_TYPE3)
ber.startSequence(0x30) // spnego.negTokenTarg
ber.startSequence(0xa2)

if (Buffer.isBuffer(this.credentials)) {
ber.writeBuffer(this.credentials, Ber.OctetString)
} else {
ber.writeString(this.credentials, Ber.OctetString)
}

ber.endSequence()
ber.endSequence()
ber.endSequence()
} else {
console.error('SASL token neither %d (for type1) nor %d (for type3) bytes long, got: %d',
SASL_TOKENLEN_TYPE1, SASL_TOKENLEN_TYPE3, this.credentials.length)
}

ber.endSequence()
ber.endSequence()
} else {
ber.writeString((this.credentials || ''), Ber.Context)
}

return ber
}
Expand Down
21 changes: 21 additions & 0 deletions lib/messages/bind_response.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const util = require('util')

const LDAPResult = require('./result')
const Protocol = require('../protocol')
const { LDAP_SASL_BIND_IN_PROGRESS } = require('../errors/codes')

/// --- API

Expand All @@ -17,6 +18,26 @@ function BindResponse (options) {
}
util.inherits(BindResponse, LDAPResult)

BindResponse.prototype._parse = function (ber) {
assert.ok(ber)

this.status = ber.readEnumeration()
this.matchedDN = ber.readString()
this.errorMessage = ber.readString()

if (this.status === LDAP_SASL_BIND_IN_PROGRESS) {
readByteString(ber, ber.buffer.length - 230)
this.saslChallange = ber.buffer
}

return true
}

function readByteString (ber, len) { return String.fromCharCode(...range(0, len - 1).map(function () { return ber.readByte() })) }
function range (start, end) {
return Array.from({ length: end - start + 1 }, (_, i) => i)
}

/// --- Exports

module.exports = BindResponse
Loading