Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@cepharum/ldap-bridge

Package Overview
Dependencies
Maintainers
2
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cepharum/ldap-bridge - npm Package Compare versions

Comparing version 0.1.0 to 0.1.1

18

backends/abstract.js

@@ -45,2 +45,10 @@ /**

/**
* Detects if provided domain name is covered by current backend.
*
* @param {string} domain domain name to test
* @returns {boolean} true if backend is covering provided domain name
*/
coversDomain( domain ) { return false; } // eslint-disable-line no-unused-vars
/**
* Detects if named user exists or not.

@@ -62,4 +70,14 @@ *

authenticate( name, token ) { return Promise.reject( new Error( "no backend" ) ); } // eslint-disable-line no-unused-vars
/**
* Qualifies attributes of entry to be returned to a searching LDAP client.
*
* @param {object} user information on user described by returned entry
* @param {object} boundUser information on currently bound LDAP user
* @param {object} attributes LDAP attributes to be qualified
* @returns {object} qualified LDAP attributes
*/
qualifyAttributes( user, boundUser, attributes ) { return attributes; }
}
exports.Backend = Backend;

@@ -75,2 +75,7 @@ /**

/** @inheritDoc */
coversDomain( domain ) {
return domain === this.domain;
}
/** @inheritDoc */
exists( name ) { // eslint-disable-line no-unused-vars

@@ -77,0 +82,0 @@ return Promise.resolve( true );

2

package.json
{
"name": "@cepharum/ldap-bridge",
"version": "0.1.0",
"version": "0.1.1",
"description": "remotely backed LDAP authentication",

@@ -5,0 +5,0 @@ "main": "server/index.js",

@@ -68,1 +68,75 @@ # ldap-bridge

In a production setup the service requires LDAP-side and backends to communicate over encrypted connections, only. You need to set **NODE_ENV** environment variable to `development` to work with non-encrypted LDAP server locally.
## Attribute Qualification
When delivering LDAP entries as results of a search query those results may be qualified depending on a found user's mail address. It supports the following mail address formats resulting in additional LDAP attribute values for **givenName**, **sn** (for surname) and **objectclass** returned:
```
givenName.surname@domain.tld
givenName.surname_objectClass@domain.tld
givenName.anotherGivenName.surname@domain.tld
givenName.anotherGivenName.surname_objectClass@domain.tld
```
Either given name and the surname are converted to have leading capitals. Dashes are supported in those parts of address as well. Multiple given names separated by period in mail address are provided space-separated in resulting **givenName** attribute.
For example, the mail address
```
ann-mary.jane.miller-smith_admin@foo.com
```
results in LDAP entry
```
dn: uid=ann-mary.jane.miller-smith_admin,dc=foo,dc=com
objectclass: top
objectclass: user
objectclass: admin
uid: ann-mary.jane.miller-smith_admin
givenName: Ann-Mary Jane
sn: Miller-Smith
```
## Fuzzy Queries
### Sub-Addressing in LDAP Searches
When searching for a user by mail address its sub-addressing part is ignored. So, searching for
```
john.doe+office@foo.com
```
results in
```
dn: uid=john.doe,dc=foo,dc=com
objectclass: top
objectclass: user
uid: john.doe
givenName: John
sn: Doe
```
The **+office** sub-address is omitted.
### Additional RDNs in Bind DN
Any additional RDN is ignored on binding as a given user. The following bind DNs result in identical authentication requests against some matching backend:
```
uid=john.doe,dc=foo,dc=com
uid=john.doe,ou=people,dc=foo,dc=com
uid=john.doe,ou=staff,ou=people,dc=foo,dc=com
```
In either case the assumed user is described as:
```json
{
"mailbox": "john.doe",
"domain": "foo.com",
"address": "john.doe@foo.com"
}
```

@@ -176,2 +176,77 @@ /**

/**
* Extracts user information from provided DN.
*
* @param {DN|string} dn DN of user to process
* @returns {{mailbox: *, address: string, domain: string, attribute: string}} extracted user information
* @throws TypeError on invalid DN
*/
static dnToUsername( dn ) {
const _dn = typeof dn === "string" ? LDAP.parseDN( dn ) : dn;
_dn.setFormat( { skipSpace: true } );
const first = _dn.rdns[0];
const segments = [];
for ( let i = 1, num = _dn.rdns.length; i < num; i++ ) {
const rdn = _dn.rdns[i];
if ( rdn.attrs.dc ) {
segments.push( rdn.attrs.dc.value );
} else if ( segments.length > 0 ) {
if ( i < num - 1 ) {
throw new TypeError( `invalid DN ${_dn} for extracting username` );
}
}
}
if ( first && segments.length > 1 ) {
const attribute = Object.keys( first.attrs )[0];
if ( attribute && DefaultUsernameAttributes.indexOf( attribute.trim().toLowerCase() ) > -1 ) {
const mailbox = first.attrs[attribute].value;
const domain = segments.join( "." );
return {
attribute,
mailbox: mailbox,
domain: domain,
address: `${mailbox}@${domain}`,
};
}
}
throw new TypeError( `invalid DN ${_dn} for extracting username` );
}
/**
* Qualifies attributes of entry to be returned to a searching LDAP client.
*
* @param {object} user information on user described by returned entry
* @param {object} boundUser information on currently bound LDAP user
* @param {object} attributes LDAP attributes to be qualified
* @returns {object} qualified LDAP attributes
*/
static qualifyAttributes( user, boundUser, attributes ) {
const match = /^([^@_+]+)\.([^@_+.]+)(?:[_@]|$)/.exec( user.mailbox.trim().toLowerCase() );
if ( match ) {
const converter = ( _, lead, letter ) => lead + letter.toUpperCase();
const overlay = {
givenName: match[1].trim().replace( /(^|-|\.)([a-z])/g, converter ).replace( /\./g, " " ),
sn: match[2].trim().replace( /(^|-)([a-z])/g, converter ),
};
const flag = user.mailbox.replace( /^.+_/, "" ).trim();
if ( flag ) {
overlay.objectclass = ( attributes.objectclass || [] ).concat( flag );
}
return Object.assign( {}, attributes, overlay );
}
return attributes;
}
/**
* Registers handlers with server instance.

@@ -189,47 +264,17 @@ *

parsedBackendDN.setFormat( { skipSpace: true } );
server.search( backendDN, ( req, res, next ) => {
debug( `searching for ${req.filter.toString()} in scope of ${backendDN}` );
let usernames;
try {
usernames = this.constructor.extractUsernames( req.filter );
} catch ( cause ) {
const msg = `processing failed: ${cause.message}`;
error( msg );
next( new LDAP.OperationsError( msg ) );
return;
}
debug( `filter is testing for ${usernames.length} username(s)` );
if ( usernames.length === 1 ) {
const { attribute, mailbox, domain: mailboxDomain } = usernames[0];
if ( mailboxDomain === backendDomain ) {
const dn = `${attribute}=${mailbox},${backendDN}`;
debug( `searching for ${req.filter.toString()} in scope of ${backendDN} yielded match ${dn}` );
res.send( {
dn,
attributes: {
objectclass: [ "top", "user" ],
[attribute]: mailbox,
},
} );
}
}
res.end();
search( req, res, next, backend, `scope of ${backendDN}`, this.constructor );
} );
server.bind( backendDN, ( req, res, next ) => {
debug( `binding ${req.authentication} as ${req.name} in scope of ${backendDN}` );
req.name.setFormat( { skipSpace: true } );
debug( `${req.logId}: binding ${req.authentication} as ${req.name} in scope of ${backendDN}` );
if ( req.authentication !== "simple" ) {
const msg = "must be simple bind";
error( msg );
error( `${req.logId}: ${msg}` );
next( new LDAP.AuthMethodNotSupportedError( msg ) );

@@ -239,5 +284,4 @@ return undefined;

const parsed = /^\s*([^\s=,]+)=([^\s,]+),\s*(.+)\s*$/.exec( req.name );
if ( !parsed ) {
error( `rejecting invalid DN ${req.name}` );
if ( !req.dn.childOf( parsedBackendDN ) ) {
error( `${req.logId}: rejecting foreign bind DN ${req.name} in scope of ${backendDN}` );
next( new LDAP.InvalidDnSyntaxError( "DN must address child node of domain" ) );

@@ -247,4 +291,8 @@ return undefined;

if ( DefaultUsernameAttributes.indexOf( parsed[1] ) < 0 ) {
error( `rejecting invalid attribute ${parsed[1]} on binding in scope of ${backendDN}` );
const rdn = req.dn.rdns[0];
const attribute = Object.keys( rdn.attrs )[0];
const value = rdn.attrs[attribute].value;
if ( DefaultUsernameAttributes.indexOf( attribute.toLowerCase() ) < 0 ) {
error( `${req.logId}: rejecting invalid attribute ${attribute} on binding in scope of ${backendDN}` );
next( new LDAP.InvalidDnSyntaxError( "invalid DN attribute" ) );

@@ -254,10 +302,4 @@ return undefined;

if ( !LDAP.parseDN( parsed[3] ).equals( parsedBackendDN ) ) {
error( `rejecting foreign DN ${req.name} in scope of ${backendDN}` );
next( new LDAP.InvalidDnSyntaxError( "DN must address child node of domain" ) );
return undefined;
}
const address = `${value}@${backendDomain}`;
const address = `${parsed[2]}@${backendDomain}`;
return backend.exists( address )

@@ -267,6 +309,9 @@ .then( exists => {

return backend.authenticate( address, req.credentials )
.then( () => res.end() );
.then( () => {
res.end();
next();
} );
}
error( `can't bind as unknown user ${address}` );
error( `${req.logId}: can't bind as unknown user ${address}` );
next( new LDAP.NoSuchObjectError( "user not found" ) );

@@ -277,3 +322,3 @@

.catch( cause => {
error( `invalid credentials rejected on binding as user ${address} in scope of ${backendDN}: ${cause.stack}` );
error( `${req.logId}: invalid credentials rejected on binding as user ${address} in scope of ${backendDN}: ${cause.stack}` );
next( cause );

@@ -285,12 +330,31 @@ } );

server.search( "cn=search", ( req, res, next ) => {
debug( `searching for ${req.filter.toString()} in all available backends` );
search( req, res, next, undefined, "all available backends", this.constructor );
} );
/**
* Commonly handles LDAP search requests.
*
* @param {LDAP.SearchRequest} req LDAP request descriptor
* @param {LDAP.SearchResponse} res LDAP response manager
* @param {function(error: ?Error):void} next callback invoked when done or on failure
* @param {?Backend} backend backend to use explicitly
* @param {string} scope label on request's scope for use in log message
* @param {class<LDAPServer>} utils context providing methods for translating data
* @returns {void}
*/
function search( req, res, next, backend, scope, utils ) {
const boundAs = req.connection.ldap.bindDN;
boundAs.setFormat( { skipSpace: true } );
debug( `${req.logId}: searching for ${req.filter.toString()} in ${scope} as ${boundAs}` );
let usernames;
try {
usernames = this.constructor.extractUsernames( req.filter );
usernames = utils.extractUsernames( req.filter );
} catch ( cause ) {
const msg = `processing failed: ${cause.message}`;
error( msg );
error( `${req.logId}: ${msg}` );
next( new LDAP.OperationsError( msg ) );

@@ -300,20 +364,30 @@ return;

debug( `filter is testing for ${usernames.length} username(s)` );
debug( `${req.logId}: filter is testing for ${usernames.length} username(s)` );
const boundUser = boundAs.equals( "" ) || boundAs.equals( "cn=anonymous" ) ? undefined : utils.dnToUsername( boundAs );
if ( usernames.length < 1 && boundUser ) {
usernames.push( boundUser );
}
if ( usernames.length === 1 ) {
const { attribute, mailbox, domain: mailboxDomain } = usernames[0];
const backend = backends.selectByName( mailboxDomain );
const user = usernames[0];
const { attribute, mailbox, domain: mailboxDomain } = user;
if ( backend ) {
const dn = `${attribute}=${mailbox},${this.constructor.domainToDN( backend.domain )}`;
if ( !boundUser || ( boundUser.mailbox === mailbox && boundUser.domain === mailboxDomain ) ) {
const _backend = backend || backends.selectByName( mailboxDomain );
debug( `searching for ${req.filter.toString()} yielded match ${dn}` );
if ( _backend && _backend.coversDomain( mailboxDomain ) ) {
const dn = `${attribute}=${mailbox},${utils.domainToDN( _backend.domain )}`;
res.send( {
dn,
attributes: {
objectclass: [ "top", "user" ],
[attribute]: mailbox,
},
} );
debug( `${req.logId}: searching for ${req.filter.toString()} yielded match ${dn}` );
res.send( {
dn,
attributes: _backend.qualifyAttributes( user, boundUser, utils.qualifyAttributes( user, boundUser, {
objectclass: [ "top", "user" ],
[attribute]: mailbox,
} ) ),
} );
}
}

@@ -323,3 +397,4 @@ }

res.end();
} );
next();
}
}

@@ -326,0 +401,0 @@ }

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc