2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
8 -- luacheck: ignore 431/log
11 local st
= require
"util.stanza";
12 local sm_bind_resource
= require
"core.sessionmanager".bind_resource
;
13 local sm_make_authenticated
= require
"core.sessionmanager".make_authenticated
;
14 local base64
= require
"util.encodings".base64
;
16 local usermanager_get_sasl_handler
= require
"core.usermanager".get_sasl_handler
;
18 local secure_auth_only
= module
:get_option_boolean("c2s_require_encryption", module
:get_option_boolean("require_encryption", false));
19 local allow_unencrypted_plain_auth
= module
:get_option_boolean("allow_unencrypted_plain_auth", false)
20 local insecure_mechanisms
= module
:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth
and {} or {"PLAIN", "LOGIN"});
21 local disabled_mechanisms
= module
:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" });
23 local log = module
._log
;
25 local xmlns_sasl
='urn:ietf:params:xml:ns:xmpp-sasl';
26 local xmlns_bind
='urn:ietf:params:xml:ns:xmpp-bind';
28 local function build_reply(status
, ret
, err_msg
)
29 local reply
= st
.stanza(status
, {xmlns
= xmlns_sasl
});
30 if status
== "failure" then
32 if err_msg
then reply
:tag("text"):text(err_msg
); end
33 elseif status
== "challenge" or status
== "success" then
37 reply
:text(base64
.encode(ret
));
40 module
:log("error", "Unknown sasl status: %s", status
);
45 local function handle_status(session
, status
, ret
, err_msg
)
46 if status
== "failure" then
47 module
:fire_event("authentication-failure", { session
= session
, condition
= ret
, text
= err_msg
});
48 session
.sasl_handler
= session
.sasl_handler
:clean_clone();
49 elseif status
== "success" then
50 local ok
, err
= sm_make_authenticated(session
, session
.sasl_handler
.username
);
52 module
:fire_event("authentication-success", { session
= session
});
53 session
.sasl_handler
= nil;
54 session
:reset_stream();
56 module
:log("warn", "SASL succeeded but username was invalid");
57 module
:fire_event("authentication-failure", { session
= session
, condition
= "not-authorized", text
= err
});
58 session
.sasl_handler
= session
.sasl_handler
:clean_clone();
59 return "failure", "not-authorized", "User authenticated successfully, but username was invalid";
62 return status
, ret
, err_msg
;
65 local function sasl_process_cdata(session
, stanza
)
66 local text
= stanza
[1];
68 text
= base64
.decode(text
);
69 --log("debug", "AUTH: %s", text:gsub("[%z\001-\008\011\012\014-\031]", " "));
71 session
.sasl_handler
= nil;
72 session
.send(build_reply("failure", "incorrect-encoding"));
76 local status
, ret
, err_msg
= session
.sasl_handler
:process(text
);
77 status
, ret
, err_msg
= handle_status(session
, status
, ret
, err_msg
);
78 local s
= build_reply(status
, ret
, err_msg
);
79 log("debug", "sasl reply: %s", s
);
84 module
:hook_tag(xmlns_sasl
, "success", function (session
)
85 if session
.type ~= "s2sout_unauthed" or session
.external_auth
~= "attempting" then return; end
86 module
:log("debug", "SASL EXTERNAL with %s succeeded", session
.to_host
);
87 session
.external_auth
= "succeeded"
88 session
:reset_stream();
89 session
:open_stream(session
.from_host
, session
.to_host
);
91 module
:fire_event("s2s-authenticated", { session
= session
, host
= session
.to_host
});
95 module
:hook_tag(xmlns_sasl
, "failure", function (session
, stanza
)
96 if session
.type ~= "s2sout_unauthed" or session
.external_auth
~= "attempting" then return; end
98 local text
= stanza
:get_child_text("text");
99 local condition
= "unknown-condition";
100 for child
in stanza
:childtags() do
101 if child
.name
~= "text" then
102 condition
= child
.name
;
106 if text
and condition
then
107 condition
= condition
.. ": " .. text
;
109 module
:log("info", "SASL EXTERNAL with %s failed: %s", session
.to_host
, condition
);
111 session
.external_auth
= "failed"
112 session
.external_auth_failure_reason
= condition
;
115 module
:hook_tag(xmlns_sasl
, "failure", function (session
, stanza
) -- luacheck: ignore 212/stanza
116 session
.log("debug", "No fallback from SASL EXTERNAL failure, giving up");
117 session
:close(nil, session
.external_auth_failure_reason
);
121 module
:hook_tag("http://etherx.jabber.org/streams", "features", function (session
, stanza
)
122 if session
.type ~= "s2sout_unauthed" or not session
.secure
then return; end
124 local mechanisms
= stanza
:get_child("mechanisms", xmlns_sasl
)
126 for mech
in mechanisms
:childtags() do
127 if mech
[1] == "EXTERNAL" then
128 module
:log("debug", "Initiating SASL EXTERNAL with %s", session
.to_host
);
129 local reply
= st
.stanza("auth", {xmlns
= xmlns_sasl
, mechanism
= "EXTERNAL"});
130 reply
:text(base64
.encode(session
.from_host
))
131 session
.sends2s(reply
)
132 session
.external_auth
= "attempting"
139 local function s2s_external_auth(session
, stanza
)
140 if session
.external_auth
~= "offered" then return end -- Unexpected request
142 local mechanism
= stanza
.attr
.mechanism
;
144 if mechanism
~= "EXTERNAL" then
145 session
.sends2s(build_reply("failure", "invalid-mechanism"));
149 if not session
.secure
then
150 session
.sends2s(build_reply("failure", "encryption-required"));
154 local text
= stanza
[1];
156 session
.sends2s(build_reply("failure", "malformed-request"));
160 text
= base64
.decode(text
);
162 session
.sends2s(build_reply("failure", "incorrect-encoding"));
166 -- The text value is either "" or equals session.from_host
167 if not ( text
== "" or text
== session
.from_host
) then
168 session
.sends2s(build_reply("failure", "invalid-authzid"));
172 -- We've already verified the external cert identity before offering EXTERNAL
173 if session
.cert_chain_status
~= "valid" or session
.cert_identity_status
~= "valid" then
174 session
.sends2s(build_reply("failure", "not-authorized"));
180 session
.external_auth
= "succeeded";
181 session
.sends2s(build_reply("success"));
182 module
:log("info", "Accepting SASL EXTERNAL identity from %s", session
.from_host
);
183 module
:fire_event("s2s-authenticated", { session
= session
, host
= session
.from_host
});
184 session
:reset_stream();
188 module
:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event
)
189 local session
, stanza
= event
.origin
, event
.stanza
;
190 if session
.type == "s2sin_unauthed" then
191 return s2s_external_auth(session
, stanza
)
194 if session
.type ~= "c2s_unauthed" or module
:get_host_type() ~= "local" then return; end
196 if session
.sasl_handler
and session
.sasl_handler
.selected
then
197 session
.sasl_handler
= nil; -- allow starting a new SASL negotiation before completing an old one
199 if not session
.sasl_handler
then
200 session
.sasl_handler
= usermanager_get_sasl_handler(module
.host
, session
);
202 local mechanism
= stanza
.attr
.mechanism
;
203 if not session
.secure
and (secure_auth_only
or insecure_mechanisms
:contains(mechanism
)) then
204 session
.send(build_reply("failure", "encryption-required"));
206 elseif disabled_mechanisms
:contains(mechanism
) then
207 session
.send(build_reply("failure", "invalid-mechanism"));
210 local valid_mechanism
= session
.sasl_handler
:select(mechanism
);
211 if not valid_mechanism
then
212 session
.send(build_reply("failure", "invalid-mechanism"));
215 return sasl_process_cdata(session
, stanza
);
217 module
:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:response", function(event
)
218 local session
= event
.origin
;
219 if not(session
.sasl_handler
and session
.sasl_handler
.selected
) then
220 session
.send(build_reply("failure", "not-authorized", "Out of order SASL element"));
223 return sasl_process_cdata(session
, event
.stanza
);
225 module
:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event
)
226 local session
= event
.origin
;
227 session
.sasl_handler
= nil;
228 session
.send(build_reply("failure", "aborted"));
232 local function tls_unique(self
)
233 return self
.userdata["tls-unique"]:getpeerfinished();
236 local mechanisms_attr
= { xmlns
='urn:ietf:params:xml:ns:xmpp-sasl' };
237 local bind_attr
= { xmlns
='urn:ietf:params:xml:ns:xmpp-bind' };
238 local xmpp_session_attr
= { xmlns
='urn:ietf:params:xml:ns:xmpp-session' };
239 module
:hook("stream-features", function(event
)
240 local origin
, features
= event
.origin
, event
.features
;
241 local log = origin
.log or log;
242 if not origin
.username
then
243 if secure_auth_only
and not origin
.secure
then
244 log("debug", "Not offering authentication on insecure connection");
247 local sasl_handler
= usermanager_get_sasl_handler(module
.host
, origin
)
248 origin
.sasl_handler
= sasl_handler
;
249 if origin
.encrypted
then
250 -- check whether LuaSec has the nifty binding to the function needed for tls-unique
251 -- FIXME: would be nice to have this check only once and not for every socket
252 if sasl_handler
.add_cb_handler
then
253 local socket
= origin
.conn
:socket();
254 if socket
.getpeerfinished
then
255 sasl_handler
:add_cb_handler("tls-unique", tls_unique
);
257 sasl_handler
["userdata"] = {
258 ["tls-unique"] = socket
;
262 local mechanisms
= st
.stanza("mechanisms", mechanisms_attr
);
263 local sasl_mechanisms
= sasl_handler
:mechanisms()
264 for mechanism
in pairs(sasl_mechanisms
) do
265 if disabled_mechanisms
:contains(mechanism
) then
266 log("debug", "Not offering disabled mechanism %s", mechanism
);
267 elseif not origin
.secure
and insecure_mechanisms
:contains(mechanism
) then
268 log("debug", "Not offering mechanism %s on insecure connection", mechanism
);
270 log("debug", "Offering mechanism %s", mechanism
);
271 mechanisms
:tag("mechanism"):text(mechanism
):up();
274 if mechanisms
[1] then
275 features
:add_child(mechanisms
);
276 elseif not next(sasl_mechanisms
) then
277 local authmod
= module
:get_option_string("authentication", "internal_plain");
278 log("error", "No available SASL mechanisms, verify that the configured authentication module '%s' is loaded and configured correctly", authmod
);
280 log("warn", "All available authentication mechanisms are either disabled or not suitable for an insecure connection");
283 features
:tag("bind", bind_attr
):tag("required"):up():up();
284 features
:tag("session", xmpp_session_attr
):tag("optional"):up():up();
288 module
:hook("s2s-stream-features", function(event
)
289 local origin
, features
= event
.origin
, event
.features
;
290 if origin
.secure
and origin
.type == "s2sin_unauthed" then
291 -- Offer EXTERNAL only if both chain and identity is valid.
292 if origin
.cert_chain_status
== "valid" and origin
.cert_identity_status
== "valid" then
293 module
:log("debug", "Offering SASL EXTERNAL");
294 origin
.external_auth
= "offered"
295 features
:tag("mechanisms", { xmlns
= xmlns_sasl
})
296 :tag("mechanism"):text("EXTERNAL")
302 module
:hook("stanza/iq/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event
)
303 local origin
, stanza
= event
.origin
, event
.stanza
;
305 if stanza
.attr
.type == "set" then
306 local bind
= stanza
.tags
[1];
307 resource
= bind
:get_child("resource");
308 resource
= resource
and #resource
.tags
== 0 and resource
[1] or nil;
310 local success
, err_type
, err
, err_msg
= sm_bind_resource(origin
, resource
);
312 origin
.send(st
.reply(stanza
)
313 :tag("bind", { xmlns
= xmlns_bind
})
314 :tag("jid"):text(origin
.full_jid
));
315 origin
.log("debug", "Resource bound: %s", origin
.full_jid
);
317 origin
.send(st
.error_reply(stanza
, err_type
, err
, err_msg
));
318 origin
.log("debug", "Resource bind failed: %s", err_msg
or err
);
323 local function handle_legacy_session(event
)
324 event
.origin
.send(st
.reply(event
.stanza
));
328 module
:hook("iq/self/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session
);
329 module
:hook("iq/host/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session
);