From 178028df021ec3eb56b0cb50896afb38f69c0008 Mon Sep 17 00:00:00 2001 From: Andrew Snare Date: Tue, 12 Nov 2013 21:07:07 +0100 Subject: [PATCH] Initial implementation. --- .gitignore | 6 +++ AUTHORS | 1 + LICENSE | 20 +++++++ README.md | 53 ++++++++++++++++++ bin/ejabberd-auth.js | 22 ++++++++ etc/ejabberd-auth.yaml | 37 +++++++++++++ lib/auth-ldap.js | 118 +++++++++++++++++++++++++++++++++++++++++ lib/auth.js | 39 ++++++++++++++ lib/ejabberd.js | 60 +++++++++++++++++++++ package.json | 29 ++++++++++ 10 files changed, 385 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 LICENSE create mode 100644 README.md create mode 100755 bin/ejabberd-auth.js create mode 100644 etc/ejabberd-auth.yaml create mode 100644 lib/auth-ldap.js create mode 100644 lib/auth.js create mode 100644 lib/ejabberd.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67a4068 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Ignore npm dependencies. +/node_modules/ + +# Ignore IntelliJ project files. +/.idea/ +/*.iml diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..65f09e3 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Andrew Snare diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9c0b3ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2013 Andrew Snare + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4a5bc8 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +eJabberd Authentication Daemon +============================== + +This package is a simple authentication daemon for [eJabberd](http://www.ejabberd.im/). +At present it implements LDAP authentication and is a drop-in replacement for the +builtin support for LDAP authentication that eJabberd provides. + +The authentication daemon should work with any eJabberd version that supports the +external authentication protocol, and any LDAP server configured to allow simple +binding. (It has been tested with eJabberd 2.1.10 and OpenLDAP 2.4.31.) + +Configuration +------------- + +Edit the file `etc/ejabberd-auth.yaml` and configure: + + - At a minimum, the base context for your server. + - Any other settings where the default is inappropriate. + +*Note that installing globally (below) will copy this file to global location.* + +Installation +------------ + +The daemon uses [NodeJS](http://nodejs.org) to run, with dependencies managed by +[npm](http://npmjs.org). To install: + + % npm -g install + +You may need to perform this as root (e.g. using `sudo`) depending on your system. + +Next edit the eJabberd configuration to use external authentication. For versions +prior to 13.10, that means setting the following: + + {auth_method, external}. + {extauth_program, "/usr/bin/ejabberd-auth"}. + +*Note: adjust the path to account for where `npm` installs global package on your +system.* + +Frequently Asked Questions +-------------------------- + +Q. eJabberd has built-in support for LDAP authentication. Why bother? + +A. The built-in support was broken on some versions packaged by Debian and Ubuntu due + to changes in the underlying Erlang environment. Having no Erlang experience and + not wishing to dive into the intricacies of custom .deb packages, I built this as + (at least) an interim stop-gap. + +Q. Are there plans to support anything beyond LDAP? + +A. If there's demand, yes. diff --git a/bin/ejabberd-auth.js b/bin/ejabberd-auth.js new file mode 100755 index 0000000..d509357 --- /dev/null +++ b/bin/ejabberd-auth.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node +// +// NodeJS script that implements an authentication daemon for eJabberd. +// +'use strict;' + +var assert = require('assert'), + etc = require('etc'), + yml = require('etc-yaml'), + conf = etc().use(yml).etc().add({ + "ejabberd-auth": { + method: 'ldap', + ldap: { + uri: 'ldap://127.0.0.1', + filter: '(objectClass=*)' + } + } + }), + ldapConf = conf.get('ejabberd-auth:ldap'); +assert.equal(conf.get('ejabberd-auth:method'), 'ldap', "LDAP is currently the only supported method."); + +require('../lib/auth-ldap').start(ldapConf); diff --git a/etc/ejabberd-auth.yaml b/etc/ejabberd-auth.yaml new file mode 100644 index 0000000..cbb2ec8 --- /dev/null +++ b/etc/ejabberd-auth.yaml @@ -0,0 +1,37 @@ +# Configuration file for ejabberd-auth. +# +# At present, only LDAP authentication is supported. +# + +# The method to use for authentication. +# (At the moment 'ldap' is the only supported method.) +# Default: ldap +#method: ldap + +ldap: + # URL to use to connect to the server. + # Default: ldap://localhost + #uri: ldap://localhost + + # If anonymous binding is disallowed, the DN (and password) to bind with + # to search for accounts. + # WARNING! If set, don't forget to secure this file: + # chown root.ejabberd ejabberd-auth.yaml && chmod 640 ejabberd-auth.yaml. + #admin: + # dn: cn=ejabberd,ou=services,dc=example,dc=com + # password: guessmeifyoucan + + # Base for searching. + # Default: none + base: dc=example,dc=com + + # Attribute to match against the username. + # Default: uuid + #uuidAttr: uuid + + # The filter to use to search for accounts. + # Default: (userPassword=*) + #filter: (userPassword=*) + +# vi: sw=4 +# vim: et diff --git a/lib/auth-ldap.js b/lib/auth-ldap.js new file mode 100644 index 0000000..7fc9eab --- /dev/null +++ b/lib/auth-ldap.js @@ -0,0 +1,118 @@ +// Copyright 2013 Andrew Snare. All rights reserved. +'use strict;'; + +var ldap = require('ldapjs'), + assert = require('assert'), + auth = require('./auth'); + +function start(options) { + var base = options.base, + admin = options.admin, + url = options.uri, + uuidAttr = options.uuidAttr, + filter = options.filter, + objectFilter = ldap.parseFilter(filter), + client = ldap.createClient({ url: url }); + + function bindEvents() { + function findJabberUser(user, callback) { + client.search(base, { + filter: new ldap.AndFilter({ + filters: [ + objectFilter, + new ldap.EqualityFilter({ + attribute: uuidAttr, + value: user + }) + ] + }), + scope: 'sub', + attributes: 'dn', + attrsOnly: true, + size: 2 + }, function(err, res) { + if (err) { + console.error("Error starting search for user " + user + ": " + err); + callback(); + } else { + var dns = []; + res.on('searchEntry', function(entry) { + dns.push(entry.object.dn); + }); + res.on('error', function(err) { + console.error("Error searching for user " + user + ": " + err); + callback(); + }); + res.on('end', function(result) { + if (result.status === ldap.LDAP_SUCCESS) { + switch (dns.length) { + case 0: + console.warn("User not found: " + user); + callback(); + break; + case 1: + var dn = dns[0]; + console.warn("User found: " + dn); + callback.apply(null, dns); + break; + case 2: + console.warn("Multiple users found; ignoring: " + user); + callback(); + break; + } + } else { + console.error("LDAP error searching for user " + user + ": " + result.status); + callback(); + } + }); + } + }); + } + + var authenticator = new auth.Authenticator(); + authenticator.on('error', function(err) { + console.error("Authenticator error: " + err); + client.unbind(); + }); + authenticator.on('end', function() { + console.warn("Stopping authenticator."); + client.unbind(); + }) + authenticator.on('isuser', function(user) { + // Here we can simply search for the user. + findJabberUser(user, function(dn) { + authenticator.channel.answer(dn !== undefined); + }); + }); + authenticator.on('auth', function(user, hostIgnored, password) { + // First we have to search the user to determine the DN. + // Assuming we find it, we then bind using the supplied password. + findJabberUser(user, function(dn) { + if (dn !== undefined) { + ldap.createClient({ url: client.url.href }) + .bind(dn, password, function(err) { + if (err && err.code !== ldap.LDAP_INVALID_CREDENTIALS) { + console.error("Unexpected authentication error: " + err); + } + authenticator.channel.answer(!err); + }); + } else { + authenticator.channel.answer(false); + } + }); + }); + } + + if (admin) { + client.bind(admin.dn, admin.password, function(err) { + assert.ifError(err); + bindEvents(); + }); + } else { + bindEvents(); + } +} + +module.exports = { + start: start +}; diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 0000000..42b55fe --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,39 @@ +// Copyright 2013 Andrew Snare. All rights reserved. +'use strict;' + +var events = require('events'), + util = require('util'); + +function Authenticator(channel) { + events.EventEmitter.call(this); + var authenticator = this; + var ejabberd = require('./ejabberd'); + authenticator.channel = channel || new ejabberd.EJabberdChannel(); + authenticator.validCommands = { + "auth": true, + "isuser": true, + "setpass": true, + "tryregister": true, + "removeuser": true, + "removeuser3": true + }; + authenticator.channel.on('command', function(command) { + console.warn("Command received: " + command + "(" + Array.prototype.slice.call(arguments).slice(1) + ")"); + if (!authenticator.validCommands[command] + || !authenticator.emit.apply(authenticator, arguments)) { + authenticator.channel.answer(false); + } + }); + authenticator.channel.on('error', function(error) { + authenticator.emit('error', "Channel error: " + error); + }); + authenticator.channel.on('end', function() { + console.warn("Channel closed."); + authenticator.emit('end'); + }); +} +util.inherits(Authenticator, events.EventEmitter); + +module.exports = { + Authenticator: Authenticator +} diff --git a/lib/ejabberd.js b/lib/ejabberd.js new file mode 100644 index 0000000..4c8e503 --- /dev/null +++ b/lib/ejabberd.js @@ -0,0 +1,60 @@ +// Copyright 2013 Andrew Snare. All rights reserved. +'use strict;' + +var events = require('events'), + util = require('util'); + +function EJabberdChannel(inStream, outStream) { + events.EventEmitter.call(this); + var channel = this; + + channel.in = inStream || process.stdin; + channel.out = outStream || process.stdout; + + channel.buffer = new Buffer(0); + + channel.in.on('data', function(chunk) { + channel.buffer = channel.decodeBuffer(Buffer.concat([channel.buffer, chunk])); + }); + channel.in.on('end', function() { + var pendingDataLength = channel.buffer.length; + if (pendingDataLength) { + channel.emit('error', "Unexpected end-of-input; " + pendingDataLength + " byte(s) not processed."); + } + channel.emit('end'); + }); + channel.in.on('error', function(error) { + channel.emit('error', "Error reading input stream: " + error); + }); + channel.out.on('error', function(error) { + channel.emit('error', "Error writing to output stream: " + error); + }) +} +util.inherits(EJabberdChannel, events.EventEmitter); +EJabberdChannel.prototype.decodeBuffer = function decodeBuffer(buffer) { + while (buffer.length > 2) { + // Check if we have the length prefix. + var commandLength = buffer.readUInt16BE(0), + commandEnd = 2 + commandLength + if (buffer.length < commandEnd) { + // We don't yet have a complete command. + break; + } + var command = buffer.toString('ascii',2,commandEnd).split(':'), + eventArguments = ['command']; + eventArguments.push.apply(eventArguments, command); + this.emit.apply(this, eventArguments); + buffer = buffer.slice(commandEnd); + } + return buffer; +} +EJabberdChannel.prototype.answer = function answer(success) { + var data = new Buffer(4); + data.writeUInt16BE(2, 0); + data.writeUInt16BE(success ? 1 : 0, 2); + this.out.write(data); +} + +module.exports = { + EJabberdChannel: EJabberdChannel +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..36ae329 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "ejabberd-auth", + "version": "1.0.0", + "description": "eJabberd Authentication Daemon", + "keywords": ["ejabberd", "ldap"], + "license": "Expat", + "directories": { + "bin": "./bin" + }, + "bin": { + "ejabberd-auth": "./bin/ejabberd-auth.js" + }, + "directories": { + "bin": "./bin", + "lib": "./lib" + }, + "dependencies": { + "ldapjs": "~0.6", + "etc": "~0.3", + "etc-yaml": "~0.0" + }, + "engines": { + "node": "~0.10", + "npm": "1" + }, + "author": "Andrew Snare ", + "preferGlobal": true, + "private": true +}