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.
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 cert_verify_identity
= require
"util.x509".verify_identity
;
18 local usermanager_get_sasl_handler
= require
"core.usermanager".get_sasl_handler
;
19 local tostring = tostring;
21 local secure_auth_only
= module
:get_option("c2s_require_encryption") or module
:get_option("require_encryption");
22 local allow_unencrypted_plain_auth
= module
:get_option("allow_unencrypted_plain_auth")
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
== "challenge" then
32 --log("debug", "CHALLENGE: %s", ret or "");
33 reply
:text(base64
.encode(ret
or ""));
34 elseif status
== "failure" then
36 if err_msg
then reply
:tag("text"):text(err_msg
); end
37 elseif status
== "success" then
38 --log("debug", "SUCCESS: %s", ret or "");
39 reply
:text(base64
.encode(ret
or ""));
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_stanza(xmlns_sasl
, "success", function (session
, stanza
)
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_stanza(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"
115 module
:hook_stanza(xmlns_sasl
, "failure", function (session
, stanza
)
116 -- TODO: Dialback wasn't loaded. Do something useful.
119 module
:hook_stanza("http://etherx.jabber.org/streams", "features", function (session
, stanza
)
120 if session
.type ~= "s2sout_unauthed" or not session
.secure
then return; end
122 local mechanisms
= stanza
:get_child("mechanisms", xmlns_sasl
)
124 for mech
in mechanisms
:childtags() do
125 if mech
[1] == "EXTERNAL" then
126 module
:log("debug", "Initiating SASL EXTERNAL with %s", session
.to_host
);
127 local reply
= st
.stanza("auth", {xmlns
= xmlns_sasl
, mechanism
= "EXTERNAL"});
128 reply
:text(base64
.encode(session
.from_host
))
129 session
.sends2s(reply
)
130 session
.external_auth
= "attempting"
137 local function s2s_external_auth(session
, stanza
)
138 local mechanism
= stanza
.attr
.mechanism
;
140 if not session
.secure
then
141 if mechanism
== "EXTERNAL" then
142 session
.sends2s(build_reply("failure", "encryption-required"))
144 session
.sends2s(build_reply("failure", "invalid-mechanism"))
149 if mechanism
~= "EXTERNAL" or session
.cert_chain_status
~= "valid" then
150 session
.sends2s(build_reply("failure", "invalid-mechanism"))
154 local text
= stanza
[1]
156 session
.sends2s(build_reply("failure", "malformed-request"))
160 -- Either the value is "=" and we've already verified the external
161 -- cert identity, or the value is a string and either matches the
164 text
= base64
.decode(text
)
166 session
.sends2s(build_reply("failure", "incorrect-encoding"))
170 if session
.cert_identity_status
== "valid" then
171 if text
~= "" and text
~= session
.from_host
then
172 session
.sends2s(build_reply("failure", "invalid-authzid"))
177 session
.sends2s(build_reply("failure", "invalid-authzid"))
181 local cert
= session
.conn
:socket():getpeercertificate()
182 if (cert_verify_identity(text
, "xmpp-server", cert
)) then
183 session
.cert_identity_status
= "valid"
185 session
.cert_identity_status
= "invalid"
186 session
.sends2s(build_reply("failure", "invalid-authzid"))
191 session
.external_auth
= "succeeded"
193 if not session
.from_host
then
194 session
.from_host
= text
;
196 session
.sends2s(build_reply("success"))
198 local domain
= text
~= "" and text
or session
.from_host
;
199 module
:log("info", "Accepting SASL EXTERNAL identity from %s", domain
);
200 module
:fire_event("s2s-authenticated", { session
= session
, host
= domain
});
201 session
:reset_stream();
205 module
:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event
)
206 local session
, stanza
= event
.origin
, event
.stanza
;
207 if session
.type == "s2sin_unauthed" then
208 return s2s_external_auth(session
, stanza
)
211 if session
.type ~= "c2s_unauthed" or module
:get_host_type() ~= "local" then return; end
213 if session
.sasl_handler
and session
.sasl_handler
.selected
then
214 session
.sasl_handler
= nil; -- allow starting a new SASL negotiation before completing an old one
216 if not session
.sasl_handler
then
217 session
.sasl_handler
= usermanager_get_sasl_handler(module
.host
, session
);
219 local mechanism
= stanza
.attr
.mechanism
;
220 if not session
.secure
and (secure_auth_only
or (mechanism
== "PLAIN" and not allow_unencrypted_plain_auth
)) then
221 session
.send(build_reply("failure", "encryption-required"));
224 local valid_mechanism
= session
.sasl_handler
:select(mechanism
);
225 if not valid_mechanism
then
226 session
.send(build_reply("failure", "invalid-mechanism"));
229 return sasl_process_cdata(session
, stanza
);
231 module
:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:response", function(event
)
232 local session
= event
.origin
;
233 if not(session
.sasl_handler
and session
.sasl_handler
.selected
) then
234 session
.send(build_reply("failure", "not-authorized", "Out of order SASL element"));
237 return sasl_process_cdata(session
, event
.stanza
);
239 module
:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event
)
240 local session
= event
.origin
;
241 session
.sasl_handler
= nil;
242 session
.send(build_reply("failure", "aborted"));
246 local mechanisms_attr
= { xmlns
='urn:ietf:params:xml:ns:xmpp-sasl' };
247 local bind_attr
= { xmlns
='urn:ietf:params:xml:ns:xmpp-bind' };
248 local xmpp_session_attr
= { xmlns
='urn:ietf:params:xml:ns:xmpp-session' };
249 module
:hook("stream-features", function(event
)
250 local origin
, features
= event
.origin
, event
.features
;
251 if not origin
.username
then
252 if secure_auth_only
and not origin
.secure
then
255 origin
.sasl_handler
= usermanager_get_sasl_handler(module
.host
, origin
);
256 local mechanisms
= st
.stanza("mechanisms", mechanisms_attr
);
257 for mechanism
in pairs(origin
.sasl_handler
:mechanisms()) do
258 if mechanism
~= "PLAIN" or origin
.secure
or allow_unencrypted_plain_auth
then
259 mechanisms
:tag("mechanism"):text(mechanism
):up();
262 if mechanisms
[1] then features
:add_child(mechanisms
); end
264 features
:tag("bind", bind_attr
):tag("required"):up():up();
265 features
:tag("session", xmpp_session_attr
):tag("optional"):up():up();
269 module
:hook("s2s-stream-features", function(event
)
270 local origin
, features
= event
.origin
, event
.features
;
271 if origin
.secure
and origin
.type == "s2sin_unauthed" then
272 -- Offer EXTERNAL if chain is valid and either we didn't validate
273 -- the identity or it passed.
274 if origin
.cert_chain_status
== "valid" and origin
.cert_identity_status
~= "invalid" then --TODO: Configurable
275 module
:log("debug", "Offering SASL EXTERNAL")
276 features
:tag("mechanisms", { xmlns
= xmlns_sasl
})
277 :tag("mechanism"):text("EXTERNAL")
283 module
:hook("iq/self/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event
)
284 local origin
, stanza
= event
.origin
, event
.stanza
;
286 if stanza
.attr
.type == "set" then
287 local bind
= stanza
.tags
[1];
288 resource
= bind
:child_with_name("resource");
289 resource
= resource
and #resource
.tags
== 0 and resource
[1] or nil;
291 local success
, err_type
, err
, err_msg
= sm_bind_resource(origin
, resource
);
293 origin
.send(st
.reply(stanza
)
294 :tag("bind", { xmlns
= xmlns_bind
})
295 :tag("jid"):text(origin
.full_jid
));
296 origin
.log("debug", "Resource bound: %s", origin
.full_jid
);
298 origin
.send(st
.error_reply(stanza
, err_type
, err
, err_msg
));
299 origin
.log("debug", "Resource bind failed: %s", err_msg
or err
);
304 local function handle_legacy_session(event
)
305 event
.origin
.send(st
.reply(event
.stanza
));
309 module
:hook("iq/self/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session
);
310 module
:hook("iq/host/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session
);