Switch to using a more generic credentials_callback/handler for SASL auth.
authornick@lupine.me.uk <nick@lupine.me.uk>
Fri, 24 Jul 2009 00:34:25 +0000 (01:34 +0100)
committernick@lupine.me.uk <nick@lupine.me.uk>
Fri, 24 Jul 2009 00:34:25 +0000 (01:34 +0100)
Not all authentication mechanisms have the same requirements; it makes sense
to provide them only with the information they require (and for them to
depend on that) so that as many auth mechanisms as possible can be supported
with a variety of credentials-storing schemes. This commit patches that together

core/usermanager.lua
plugins/mod_saslauth.lua
util/sasl.lua

index bd7772cae8469b1dc567798ac0365b2b976b46cc..c7136e8fe300c956c91bd6cecffb1df8fc89be5a 100644 (file)
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2009 Matthew Wild
 -- Copyright (C) 2008-2009 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -23,6 +23,7 @@ module "usermanager"
 function validate_credentials(host, username, password, method)
        log("debug", "User '%s' is being validated", username);
        local credentials = datamanager.load(username, host, "accounts") or {};
+
        if method == nil then method = "PLAIN"; end
        if method == "PLAIN" and credentials.password then -- PLAIN, do directly
                if password == credentials.password then
@@ -30,7 +31,7 @@ function validate_credentials(host, username, password, method)
                else
                        return nil, "Auth failed. Invalid username or password.";
                end
-       end
+  end
        -- must do md5
        -- make credentials md5
        local pwd = credentials.password;
@@ -49,6 +50,10 @@ function validate_credentials(host, username, password, method)
        end
 end
 
+function get_password(username, host)
+  return (datamanager.load(username, host, "accounts") or {}).password
+end
+
 function user_exists(username, host)
        return datamanager.load(username, host, "accounts") ~= nil; -- FIXME also check for empty credentials
 end
@@ -58,7 +63,7 @@ function create_user(username, password, host)
 end
 
 function get_supported_methods(host)
-       local methods = {["PLAIN"] = true}; -- TODO this should be taken from the config
+  local methods = {["PLAIN"] = true}; -- TODO this should be taken from the config
        methods["DIGEST-MD5"] = true;
        return methods;
 end
index 56837b99a0c2c3e3e492f214ebda61c40962ab68..87f242739eafc66e6c48150b940a19dae53113fe 100644 (file)
@@ -16,6 +16,8 @@ local base64 = require "util.encodings".base64;
 local datamanager_load = require "util.datamanager".load;
 local usermanager_validate_credentials = require "core.usermanager".validate_credentials;
 local usermanager_get_supported_methods = require "core.usermanager".get_supported_methods;
+local usermanager_user_exists = require "core.usermanager".user_exists;
+local usermanager_get_password = require "core.usermanager".get_password;
 local t_concat, t_insert = table.concat, table.insert;
 local tostring = tostring;
 local jid_split = require "util.jid".split
@@ -65,18 +67,19 @@ local function handle_status(session, status)
        end
 end
 
-local function password_callback(node, hostname, realm, mechanism, decoder)
-       local password = (datamanager_load(node, hostname, "accounts") or {}).password; -- FIXME handle hashed passwords
-       local func = function(x) return x; end;
-       if password then
-               if mechanism == "PLAIN" then
-                       return func, password;
-               elseif mechanism == "DIGEST-MD5" then
-                       if decoder then node, realm, password = decoder(node), decoder(realm), decoder(password); end
-                       return func, md5(node..":"..realm..":"..password);
-               end
-       end
-       return func, nil;
+local function credentials_callback(mechanism, ...)
+  if mechanism == "PLAIN" then
+    local username, hostname, password = arg[1], arg[2], arg[3];
+    local response = usermanager_validate_credentials(hostname, username, password, mechanism)
+    if response == nil then return false
+    else return response end
+  elseif mechanism == "DIGEST-MD5" then
+    function func(x) return x; end
+    local node, domain, realm, decoder = arg[1], arg[2], arg[3], arg[4];
+    local password = usermanager_get_password(node, domain)
+    if decoder then node, realm, password = decoder(node), decoder(realm), decoder(password); end
+    return func, md5(node..":"..realm..":"..password);
+  end
 end
 
 local function sasl_handler(session, stanza)
@@ -89,7 +92,7 @@ local function sasl_handler(session, stanza)
                elseif stanza.attr.mechanism == "ANONYMOUS" then
                        return session.send(build_reply("failure", "mechanism-too-weak"));
                end
-               session.sasl_handler = new_sasl(stanza.attr.mechanism, session.host, password_callback);
+               session.sasl_handler = new_sasl(stanza.attr.mechanism, session.host, credentials_callback);
                if not session.sasl_handler then
                        return session.send(build_reply("failure", "invalid-mechanism"));
                end
index 0082b9ccf9f074181bfc01600b7283d43889f11e..48412ea7189802575dbcbb43012e732ad849e2d0 100644 (file)
@@ -1,14 +1,14 @@
 -- sasl.lua v0.4
 -- Copyright (C) 2008-2009 Tobias Markmann
--- 
+--
 --    All rights reserved.
---    
+--
 --    Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
---    
+--
 --        * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 --        * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 --        * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
---    
+--
 --    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 
@@ -30,47 +30,45 @@ local print = print
 
 module "sasl"
 
-local function new_plain(realm, password_handler)
-       local object = { mechanism = "PLAIN", realm = realm, password_handler = password_handler}
+-- Credentials handler:
+--   Arguments: ("PLAIN", user, host, password)
+--   Returns: true (success) | false (fail) | nil (user unknown)
+local function new_plain(realm, credentials_handler)
+       local object = { mechanism = "PLAIN", realm = realm, credentials_handler = credentials_handler}
        function object.feed(self, message)
-       
                if message == "" or message == nil then return "failure", "malformed-request" end
                local response = message
                local authorization = s_match(response, "([^&%z]+)")
                local authentication = s_match(response, "%z([^&%z]+)%z")
                local password = s_match(response, "%z[^&%z]+%z([^&%z]+)")
-               
-               if authentication == nil or password == nil then return "failure", "malformed-request" end
-               
-               local password_encoding, correct_password = self.password_handler(authentication, self.realm, self.realm, "PLAIN")
-               
-               if correct_password == nil then return "failure", "not-authorized"
-               elseif correct_password == false then return "failure", "account-disabled" end
-               
-               local claimed_password = ""
-               if password_encoding == nil then claimed_password = password
-               else claimed_password = password_encoding(password) end
-               
-               self.username = authentication
-               if claimed_password == correct_password then
-                       return "success"
-               else
-                       return "failure", "not-authorized"
-               end
-       end
-       return object
-end
 
+    if authentication == nil or password == nil then return "failure", "malformed-request" end
+    self.username = authentication
+    local auth_success = self.credentials_handler("PLAIN", self.username, self.realm, password)
+
+    if auth_success then
+      return "success"
+    elseif auth_success == nil then
+      return "failure", "account-disabled"
+    else
+      return "failure", "not-authorized"
+    end
+  end
+  return object
+end
 
+-- credentials_handler:
+--   Arguments: (mechanism, node, domain, realm, decoder)
+--   Returns: Password encoding, (plaintext) password
 -- implementing RFC 2831
-local function new_digest_md5(realm, password_handler)
+local function new_digest_md5(realm, credentials_handler)
        --TODO complete support for authzid
 
        local function serialize(message)
                local data = ""
-               
+
                if type(message) ~= "table" then error("serialize needs an argument of type table.") end
-               
+
                -- testing all possible values
                if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end
                if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end
@@ -81,7 +79,7 @@ local function new_digest_md5(realm, password_handler)
                data = data:gsub(",$", "")
                return data
        end
-       
+
        local function utf8tolatin1ifpossible(passwd)
                local i = 1;
                while i <= #passwd do
@@ -137,16 +135,16 @@ local function new_digest_md5(realm, password_handler)
                return message;
        end
 
-       local object = { mechanism = "DIGEST-MD5", realm = realm, password_handler = password_handler};
-       
+       local object = { mechanism = "DIGEST-MD5", realm = realm, credentials_handler = credentials_handler};
+
        object.nonce = generate_uuid();
        object.step = 0;
        object.nonce_count = {};
-                                                                                               
+
        function object.feed(self, message)
                self.step = self.step + 1;
                if (self.step == 1) then
-                       local challenge = serialize({   nonce = object.nonce, 
+                       local challenge = serialize({   nonce = object.nonce,
                                                                                        qop = "auth",
                                                                                        charset = "utf-8",
                                                                                        algorithm = "md5-sess",
@@ -158,13 +156,13 @@ local function new_digest_md5(realm, password_handler)
                        if response["nc"] then
                                if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end
                        end
-                       
+
                        -- check for username, it's REQUIRED by RFC 2831
                        if not response["username"] then
                                return "failure", "malformed-request";
                        end
                        self["username"] = response["username"];
-                       
+
                        -- check for nonce, ...
                        if not response["nonce"] then
                                return "failure", "malformed-request";
@@ -172,23 +170,23 @@ local function new_digest_md5(realm, password_handler)
                                -- check if it's the right nonce
                                if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end
                        end
-                       
+
                        if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end
                        if not response["qop"] then response["qop"] = "auth" end
-                       
+
                        if response["realm"] == nil or response["realm"] == "" then
                                response["realm"] = "";
                        elseif response["realm"] ~= self.realm then
                                return "failure", "not-authorized", "Incorrect realm value";
                        end
-                       
+
                        local decoder;
                        if response["charset"] == nil then
                                decoder = utf8tolatin1ifpossible;
                        elseif response["charset"] ~= "utf-8" then
                                return "failure", "incorrect-encoding", "The client's response uses "..response["charset"].." for encoding with isn't supported by sasl.lua. Supported encodings are latin or utf-8.";
                        end
-                       
+
                        local domain = "";
                        local protocol = "";
                        if response["digest-uri"] then
@@ -197,10 +195,10 @@ local function new_digest_md5(realm, password_handler)
                        else
                                return "failure", "malformed-request", "Missing entry for digest-uri in SASL message."
                        end
-                       
+
                        --TODO maybe realm support
                        self.username = response["username"];
-                       local password_encoding, Y = self.password_handler(response["username"], to_unicode(domain), response["realm"], "DIGEST-MD5", decoder);
+                       local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], to_unicode(domain), response["realm"], decoder);
                        if Y == nil then return "failure", "not-authorized"
                        elseif Y == false then return "failure", "account-disabled" end
                        local A1 = "";
@@ -216,27 +214,27 @@ local function new_digest_md5(realm, password_handler)
                                A1 = Y..":"..response["nonce"]..":"..response["cnonce"];
                        end
                        local A2 = "AUTHENTICATE:"..protocol.."/"..domain;
-                       
+
                        local HA1 = md5(A1, true);
                        local HA2 = md5(A2, true);
-                       
+
                        local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2;
                        local response_value = md5(KD, true);
-                       
+
                        if response_value == response["response"] then
                                -- calculate rspauth
                                A2 = ":"..protocol.."/"..domain;
-                               
+
                                HA1 = md5(A1, true);
                                HA2 = md5(A2, true);
-                               
+
                                KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
                                local rspauth = md5(KD, true);
                                self.authenticated = true;
                                return "challenge", serialize({rspauth = rspauth});
                        else
                                return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."
-                       end                                                     
+                       end
                elseif self.step == 3 then
                        if self.authenticated ~= nil then return "success"
                        else return "failure", "malformed-request" end
@@ -245,8 +243,10 @@ local function new_digest_md5(realm, password_handler)
        return object;
 end
 
-local function new_anonymous(realm, password_handler)
-       local object = { mechanism = "ANONYMOUS", realm = realm, password_handler = password_handler}
+-- Credentials handler: Can be nil. If specified, should take the mechanism as
+-- the only argument, and return true for OK, or false for not-OK (TODO)
+local function new_anonymous(realm, credentials_handler)
+       local object = { mechanism = "ANONYMOUS", realm = realm, credentials_handler = credentials_handler}
                function object.feed(self, message)
                        return "success"
                end
@@ -255,11 +255,11 @@ local function new_anonymous(realm, password_handler)
 end
 
 
-function new(mechanism, realm, password_handler)
+function new(mechanism, realm, credentials_handler)
        local object
-       if mechanism == "PLAIN" then object = new_plain(realm, password_handler)
-       elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, password_handler)
-       elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, password_handler)
+       if mechanism == "PLAIN" then object = new_plain(realm, credentials_handler)
+       elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, credentials_handler)
+       elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, credentials_handler)
        else
                log("debug", "Unsupported SASL mechanism: "..tostring(mechanism));
                return nil