changeset 1196:f45ca6edc159

mod_auth_imap: Authentication module that works by passing through SASL to a IMAP connection
author Kim Alvefur <zash@zash.se>
date Thu, 26 Sep 2013 13:43:27 +0200
parents f502cbffbdd4
children 25641c4cab36
files mod_auth_imap/auth_imap/mod_auth_imap.lua mod_auth_imap/auth_imap/sasl_imap.lib.lua
diffstat 2 files changed, 238 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_auth_imap/auth_imap/mod_auth_imap.lua	Thu Sep 26 13:43:27 2013 +0200
@@ -0,0 +1,67 @@
+-- IMAP authentication backend for Prosody
+--
+-- Copyright (C) 2011 FIMXE from hg annotate -u
+
+local name = "IMAP SASL";
+local log = require "util.logger".init("auth_imap");
+
+local imap_host = module:get_option_string("imap_auth_host", "localhost");
+local imap_port = module:get_option_number("imap_auth_port", 143);
+
+
+local imap_service_realm = module:get_option("imap_service_realm");
+local imap_service_name = module:get_option("imap_service_name");
+
+
+local new_imap_sasl = module:require "sasl_imap".new;
+
+local new_sasl = function(realm)
+	return new_imap_sasl(
+		imap_service_realm or realm,
+		imap_service_name or "xmpp",
+		imap_host, imap_port
+	);
+end
+
+do
+	local s = new_sasl(module.host)
+	assert(s, "Could not create a new SASL object");
+	assert(s.mechanisms, "SASL object has no mechanims method");
+	local m = {};
+	for k in pairs(s:mechanisms()) do
+		table.insert(m, k);
+	end
+	log("debug", "Mechanims found: %s", table.concat(m, ", "));
+end
+
+provider = {
+	name = module.name:gsub("^auth_","");
+};
+
+function provider.test_password(username, password)
+	return nil, "Legacy auth not supported with "..name;
+end
+
+function provider.get_password(username)
+	return nil, "Passwords unavailable for "..name;
+end
+
+function provider.set_password(username, password)
+	return nil, "Passwords unavailable for "..name;
+end
+
+function provider.user_exists(username)
+	-- FIXME
+	return true
+end
+
+function provider.create_user(username, password)
+	return nil, "Account creation/modification not available with "..name;
+end
+
+function provider.get_sasl_handler()
+	return new_sasl(module.host);
+end
+
+module:add_item("auth-provider", provider);
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_auth_imap/auth_imap/sasl_imap.lib.lua	Thu Sep 26 13:43:27 2013 +0200
@@ -0,0 +1,171 @@
+-- Dovecot authentication backend for Prosody
+--
+-- Copyright (C) 2011 Kim Alvefur
+--
+
+local log = require "util.logger".init("sasl_imap");
+
+local setmetatable = setmetatable;
+
+local s_match, s_gmatch = string.match, string.gmatch
+local t_concat = table.concat;
+local m_random = math.random;
+local tostring, tonumber = tostring, tonumber;
+
+local socket = require "socket"
+-- TODO -- local ssl = require "ssl"
+local base64 = require "util.encodings".base64;
+local b64, unb64 = base64.encode, base64.decode;
+
+local _M = {};
+
+local method = {};
+method.__index = method;
+
+-- For extracting the username.
+local mitm = {
+	PLAIN = function(message)
+		return s_match(message, "^[^%z]*%z([^%z]+)%z[^%z]+");
+	end,
+	["SCRAM-SHA-1"] = function(message)
+		return s_match(message, "^[^,]+,[^,]*,n=([^,]*)");
+	end,
+	["DIGEST-MD5"] = function(message)
+		return s_match(message, "username=\"([^\"]*)\"");
+	end,
+}
+
+local function connect(host, port, ssl)
+	port = tonumber(port) or (ssl and 993 or 143);
+	log("debug", "connect() to %s:%s:%d", ssl and "ssl" or "tcp", host, tonumber(port));
+	local conn = socket.tcp();
+
+	-- Create a connection to imap socket
+	log("debug", "connecting to imap at '%s:%d'", host, port);
+	local ok, err = conn:connect(host, port);
+	conn:settimeout(10);
+	if not ok then
+		log("error", "error connecting to imap at '%s:%d'. error was '%s'. check permissions", host, port, err);
+		return false;
+	end
+
+	-- Parse IMAP handshake
+	local done = false;
+	local supported_mechs = {};
+	local line = conn:receive("*l");
+	log("debug", "imap handshake: '%s'", line);
+	if not line then
+		return false;
+	end
+	local caps = line:match("^%*%s+OK%s+(%b[])");
+	if caps then
+		caps = caps:sub(2,-2);
+		for cap in caps:gmatch("%S+") do
+			log("debug", "Capability: %s", cap);
+			local mech = cap:match("AUTH=(.*)");
+			if mech then
+				log("debug", "Supported SASL mechanism: %s", mech);
+				supported_mechs[mech] = mitm[mech] and true or nil; 
+			end
+		end
+	end
+
+	return conn, supported_mechs;
+end
+
+-- create a new SASL object which can be used to authenticate clients
+function _M.new(realm, service_name, host, port, ssl)
+	log("debug", "new(%q, %q, %q, %d)", realm or "", service_name or "", host or "", port or 0);
+	local sasl_i = {
+		realm = realm,
+		service_name = service_name,
+		_host = host,
+		_port = port,
+		_ssl = ssl
+	};
+
+	local conn, mechs = connect(host, port, ssl);
+	if not conn then
+		return nil, "Socket connection failure";
+	end
+	sasl_i.conn, sasl_i.mechs = conn, mechs;
+	return setmetatable(sasl_i, method);
+end
+
+-- get a fresh clone with the same realm and service name
+function method:clean_clone()
+	if self.conn then
+		self.conn:close();
+		self.conn = nil;
+	end
+	log("debug", "method:clean_clone()");
+	return _M.new(self.realm, self.service_name, self._host, self._port)
+end
+
+-- get a list of possible SASL mechanisms to use
+function method:mechanisms()
+	log("debug", "method:mechanisms()");
+	return self.mechs;
+end
+
+-- select a mechanism to use
+function method:select(mechanism)
+	log("debug", "method:select(%q)", mechanism);
+	if not self.selected and self.mechs[mechanism] then
+		self.tag = tostring({}):match("0x(%x*)$");
+		self.selected = mechanism;
+		local selectmsg = t_concat({ self.tag, "AUTHENTICATE", mechanism }, " ");
+		log("debug", "Sending %d bytes: %q", #selectmsg, selectmsg);
+		local ok, err = self.conn:send(selectmsg.."\n");
+		if not ok then
+			log("error", "Could not write to socket: %s", err);
+			return "failure", "internal-server-error", err
+		end
+		local line, err = self.conn:receive("*l");
+		if not line then
+			log("error", "Could not read from socket: %s", err);
+			return "failure", "internal-server-error", err
+		end
+		log("debug", "Received %d bytes: %q", #line, line);
+		return line:match("^+")
+	end
+end
+
+-- feed new messages to process into the library
+function method:process(message)
+	local username = mitm[self.selected](message);
+	if username then self.username = username; end
+	log("debug", "method:process(%d bytes)", #message);
+	local ok, err = self.conn:send(b64(message).."\n");
+	if not ok then
+		log("error", "Could not write to socket: %s", err);
+		return "failure", "internal-server-error", err
+	end
+	log("debug", "Sent %d bytes to socket", ok);
+	local line, err = self.conn:receive("*l");
+	if not line then
+		log("error", "Could not read from socket: %s", err);
+		return "failure", "internal-server-error", err
+	end
+	log("debug", "Received %d bytes from socket: %s", #line, line);
+
+	if line:match("^%+") and #line > 2 then
+		local data = line:sub(3);
+		data = data and unb64(data);
+		return "challenge", unb64(data);
+	elseif line:sub(1, #self.tag) == self.tag then
+		local ok, rest = line:sub(#self.tag+1):match("(%w+)%s+(.*)");
+		ok = ok:lower();
+		log("debug", "%s: %s", ok, rest);
+		if ok == "ok" then
+			return "success"
+		elseif ok == "no" then
+			return "failure", "not-authorized", rest;
+		end
+	elseif line:match("^%* BYE") then
+		local err = line:match("BYE%s*(.*)");
+		return "failure", "not-authorized", err;
+	end
+end
+
+return _M;