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
;
17 local tostring = tostring;
19 local secure_auth_only
= module
:get_option_boolean("c2s_require_encryption", module
:get_option_boolean("require_encryption", false));
20 local allow_unencrypted_plain_auth
= module
:get_option_boolean("allow_unencrypted_plain_auth", false)
21 local insecure_mechanisms
= module
:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth
and {} or {"PLAIN", "LOGIN"});
22 local disabled_mechanisms
= module
:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" });
24 local log = module
._log
;
26 local xmlns_sasl
='urn:ietf:params:xml:ns:xmpp-sasl';
27 local xmlns_bind
='urn:ietf:params:xml:ns:xmpp-bind';
29 local function build_reply(status
, ret
, err_msg
)
30 local reply
= st
.stanza(status
, {xmlns
= xmlns_sasl
});
31 if status
== "failure" then
33 if err_msg
then reply
:tag("text"):text(err_msg
); end
34 elseif status
== "challenge" or status
== "success" then
38 reply
:text(base64
.encode(ret
));
41 module
:log("error", "Unknown sasl status: %s", status
);
46 local function handle_status(session
, status
, ret
, err_msg
)
47 if status
== "failure" then
48 module
:fire_event("authentication-failure", { session
= session
, condition
= ret
, text
= err_msg
});
49 session
.sasl_handler
= session
.sasl_handler
:clean_clone();
50 elseif status
== "success" then
51 local ok
, err
= sm_make_authenticated(session
, session
.sasl_handler
.username
);
53 module
:fire_event("authentication-success", { session
= session
});
54 session
.sasl_handler
= nil;
55 session
:reset_stream();
57 module
:log("warn", "SASL succeeded but username was invalid");
58 module
:fire_event("authentication-failure", { session
= session
, condition
= "not-authorized", text
= err
});
59 session
.sasl_handler
= session
.sasl_handler
:clean_clone();
60 return "failure", "not-authorized", "User authenticated successfully, but username was invalid";
63 return status
, ret
, err_msg
;
66 local function sasl_process_cdata(session
, stanza
)
67 local text
= stanza
[1];
69 text
= base64
.decode(text
);
70 --log("debug", "AUTH: %s", text:gsub("[%z\001-\008\011\012\014-\031]", " "));
72 session
.sasl_handler
= nil;
73 session
.send(build_reply("failure", "incorrect-encoding"));
77 local status
, ret
, err_msg
= session
.sasl_handler
:process(text
);
78 status
, ret
, err_msg
= handle_status(session
, status
, ret
, err_msg
);
79 local s
= build_reply(status
, ret
, err_msg
);
80 log("debug", "sasl reply: %s", tostring(s
));
85 module
:hook_tag(xmlns_sasl
, "success", function (session
)
86 if session
.type ~= "s2sout_unauthed" or session
.external_auth
~= "attempting" then return; end
87 module
:log("debug", "SASL EXTERNAL with %s succeeded", session
.to_host
);
88 session
.external_auth
= "succeeded"
89 session
:reset_stream();
90 session
:open_stream(session
.from_host
, session
.to_host
);
92 module
:fire_event("s2s-authenticated", { session
= session
, host
= session
.to_host
});
96 module
:hook_tag(xmlns_sasl
, "failure", function (session
, stanza
)
97 if session
.type ~= "s2sout_unauthed" or session
.external_auth
~= "attempting" then return; end
99 local text
= stanza
:get_child_text("text");
100 local condition
= "unknown-condition";
101 for child
in stanza
:childtags() do
102 if child
.name
~= "text" then
103 condition
= child
.name
;
107 if text
and condition
then
108 condition
= condition
.. ": " .. text
;
110 module
:log("info", "SASL EXTERNAL with %s failed: %s", session
.to_host
, condition
);
112 session
.external_auth
= "failed"
113 session
.external_auth_failure_reason
= condition
;
116 module
:hook_tag(xmlns_sasl
, "failure", function (session
, stanza
) -- luacheck: ignore 212/stanza
117 session
.log("debug", "No fallback from SASL EXTERNAL failure, giving up");
118 session
:close(nil, session
.external_auth_failure_reason
);
122 module
:hook_tag("http://etherx.jabber.org/streams", "features", function (session
, stanza
)
123 if session
.type ~= "s2sout_unauthed" or not session
.secure
then return; end
125 local mechanisms
= stanza
:get_child("mechanisms", xmlns_sasl
)
127 for mech
in mechanisms
:childtags() do
128 if mech
[1] == "EXTERNAL" then
129 module
:log("debug", "Initiating SASL EXTERNAL with %s", session
.to_host
);
130 local reply
= st
.stanza("auth", {xmlns
= xmlns_sasl
, mechanism
= "EXTERNAL"});
131 reply
:text(base64
.encode(session
.from_host
))
132 session
.sends2s(reply
)
133 session
.external_auth
= "attempting"
140 local function s2s_external_auth(session
, stanza
)
141 if session
.external_auth
~= "offered" then return end -- Unexpected request
143 local mechanism
= stanza
.attr
.mechanism
;
145 if mechanism
~= "EXTERNAL" then
146 session
.sends2s(build_reply("failure", "invalid-mechanism"));
150 if not session
.secure
then
151 session
.sends2s(build_reply("failure", "encryption-required"));
155 local text
= stanza
[1];
157 session
.sends2s(build_reply("failure", "malformed-request"));
161 text
= base64
.decode(text
);
163 session
.sends2s(build_reply("failure", "incorrect-encoding"));
167 -- The text value is either "" or equals session.from_host
168 if not ( text
== "" or text
== session
.from_host
) then
169 session
.sends2s(build_reply("failure", "invalid-authzid"));
173 -- We've already verified the external cert identity before offering EXTERNAL
174 if session
.cert_chain_status
~= "valid" or session
.cert_identity_status
~= "valid" then
175 session
.sends2s(build_reply("failure", "not-authorized"));
181 session
.external_auth
= "succeeded";
182 session
.sends2s(build_reply("success"));
183 module
:log("info", "Accepting SASL EXTERNAL identity from %s", session
.from_host
);
184 module
:fire_event("s2s-authenticated", { session
= session
, host
= session
.from_host
});
185 session
:reset_stream();
189 module
:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event
)
190 local session
, stanza
= event
.origin
, event
.stanza
;
191 if session
.type == "s2sin_unauthed" then
192 return s2s_external_auth(session
, stanza
)
195 if session
.type ~= "c2s_unauthed" or module
:get_host_type() ~= "local" then return; end
197 if session
.sasl_handler
and session
.sasl_handler
.selected
then
198 session
.sasl_handler
= nil; -- allow starting a new SASL negotiation before completing an old one
200 if not session
.sasl_handler
then
201 session
.sasl_handler
= usermanager_get_sasl_handler(module
.host
, session
);
203 local mechanism
= stanza
.attr
.mechanism
;
204 if not session
.secure
and (secure_auth_only
or insecure_mechanisms
:contains(mechanism
)) then
205 session
.send(build_reply("failure", "encryption-required"));
207 elseif disabled_mechanisms
:contains(mechanism
) then
208 session
.send(build_reply("failure", "invalid-mechanism"));
211 local valid_mechanism
= session
.sasl_handler
:select(mechanism
);
212 if not valid_mechanism
then
213 session
.send(build_reply("failure", "invalid-mechanism"));
216 return sasl_process_cdata(session
, stanza
);
218 module
:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:response", function(event
)
219 local session
= event
.origin
;
220 if not(session
.sasl_handler
and session
.sasl_handler
.selected
) then
221 session
.send(build_reply("failure", "not-authorized", "Out of order SASL element"));
224 return sasl_process_cdata(session
, event
.stanza
);
226 module
:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event
)
227 local session
= event
.origin
;
228 session
.sasl_handler
= nil;
229 session
.send(build_reply("failure", "aborted"));
233 local function tls_unique(self
)
234 return self
.userdata["tls-unique"]:getpeerfinished();
237 local mechanisms_attr
= { xmlns
='urn:ietf:params:xml:ns:xmpp-sasl' };
238 local bind_attr
= { xmlns
='urn:ietf:params:xml:ns:xmpp-bind' };
239 local xmpp_session_attr
= { xmlns
='urn:ietf:params:xml:ns:xmpp-session' };
240 module
:hook("stream-features", function(event
)
241 local origin
, features
= event
.origin
, event
.features
;
242 local log = origin
.log or log;
243 if not origin
.username
then
244 if secure_auth_only
and not origin
.secure
then
245 log("debug", "Not offering authentication on insecure connection");
248 local sasl_handler
= usermanager_get_sasl_handler(module
.host
, origin
)
249 origin
.sasl_handler
= sasl_handler
;
250 if origin
.encrypted
then
251 -- check wether LuaSec has the nifty binding to the function needed for tls-unique
252 -- FIXME: would be nice to have this check only once and not for every socket
253 if sasl_handler
.add_cb_handler
then
254 local socket
= origin
.conn
:socket();
255 if socket
.getpeerfinished
then
256 sasl_handler
:add_cb_handler("tls-unique", tls_unique
);
258 sasl_handler
["userdata"] = {
259 ["tls-unique"] = socket
;
263 local mechanisms
= st
.stanza("mechanisms", mechanisms_attr
);
264 local sasl_mechanisms
= sasl_handler
:mechanisms()
265 for mechanism
in pairs(sasl_mechanisms
) do
266 if disabled_mechanisms
:contains(mechanism
) then
267 log("debug", "Not offering disabled mechanism %s", mechanism
);
268 elseif not origin
.secure
and insecure_mechanisms
:contains(mechanism
) then
269 log("debug", "Not offering mechanism %s on insecure connection", mechanism
);
271 log("debug", "Offering mechanism %s", mechanism
);
272 mechanisms
:tag("mechanism"):text(mechanism
):up();
275 if mechanisms
[1] then
276 features
:add_child(mechanisms
);
277 elseif not next(sasl_mechanisms
) then
278 log("warn", "No available SASL mechanisms, verify that the configured authentication module is working");
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
);