Mercurial > prosody-modules
annotate mod_email_pass/mod_email_pass.lua @ 5549:01a0b67a9afd
mod_http_oauth2: Add TODO about disabling password grant
Per recommendation in draft-ietf-oauth-security-topics-23 it should at
the very least be disabled by default.
However since this is used by the Snikket web portal some care needs to
be taken not to break this, unless it's already broken by other changes
to this module.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Fri, 16 Jun 2023 00:06:53 +0200 |
parents | c60e9943dcb9 |
children |
rev | line source |
---|---|
1342 | 1 local dm_load = require "util.datamanager".load; |
2 local st = require "util.stanza"; | |
3 local nodeprep = require "util.encodings".stringprep.nodeprep; | |
4 local usermanager = require "core.usermanager"; | |
5 local http = require "net.http"; | |
6 local vcard = module:require "vcard"; | |
7 local datetime = require "util.datetime"; | |
8 local timer = require "util.timer"; | |
9 local jidutil = require "util.jid"; | |
10 | |
11 -- SMTP related params. Readed from config | |
12 local os_time = os.time; | |
13 local smtp = require "socket.smtp"; | |
14 local smtp_server = module:get_option_string("smtp_server", "localhost"); | |
15 local smtp_port = module:get_option_string("smtp_port", "25"); | |
16 local smtp_ssl = module:get_option_boolean("smtp_ssl", false); | |
17 local smtp_user = module:get_option_string("smtp_username"); | |
18 local smtp_pass = module:get_option_string("smtp_password"); | |
19 local smtp_address = module:get_option("smtp_from") or ((smtp_user or "no-responder").."@"..(smtp_server or module.host)); | |
20 local mail_subject = module:get_option_string("msg_subject") | |
21 local mail_body = module:get_option_string("msg_body"); | |
22 local url_path = module:get_option_string("url_path", "/resetpass"); | |
23 | |
24 | |
25 -- This table has the tokens submited by the server | |
26 tokens_mails = {}; | |
27 tokens_expiration = {}; | |
28 | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
29 -- URL |
1342 | 30 local https_host = module:get_option_string("https_host"); |
31 local http_host = module:get_option_string("http_host"); | |
32 local https_port = module:get_option("https_ports", { 443 }); | |
33 local http_port = module:get_option("http_ports", { 80 }); | |
34 | |
35 local timer_repeat = 120; -- repeat after 120 secs | |
36 | |
37 function enablessl() | |
38 local sock = socket.tcp() | |
39 return setmetatable({ | |
40 connect = function(_, host, port) | |
41 local r, e = sock:connect(host, port) | |
42 if not r then return r, e end | |
43 sock = ssl.wrap(sock, {mode='client', protocol='tlsv1'}) | |
44 return sock:dohandshake() | |
45 end | |
46 }, { | |
47 __index = function(t,n) | |
48 return function(_, ...) | |
49 return sock[n](sock, ...) | |
50 end | |
51 end | |
52 }) | |
53 end | |
54 | |
55 function template(data) | |
56 -- Like util.template, but deals with plain text | |
57 return { apply = function(values) return (data:gsub("{([^}]+)}", values)); end } | |
58 end | |
59 | |
60 local function get_template(name, extension) | |
61 local fh = assert(module:load_resource("templates/"..name..extension)); | |
62 local data = assert(fh:read("*a")); | |
63 fh:close(); | |
64 return template(data); | |
65 end | |
66 | |
67 local function render(template, data) | |
68 return tostring(template.apply(data)); | |
69 end | |
70 | |
71 function send_email(address, smtp_address, message_text, subject) | |
72 local rcpt = "<"..address..">"; | |
73 | |
74 local mesgt = { | |
75 headers = { | |
76 to = address; | |
77 subject = subject or ("Jabber password reset "..jid_bare(from_address)); | |
78 }; | |
79 body = message_text; | |
80 }; | |
81 local ok, err = nil; | |
82 | |
83 if not smtp_ssl then | |
84 ok, err = smtp.send{ from = smtp_address, rcpt = rcpt, source = smtp.message(mesgt), | |
85 server = smtp_server, user = smtp_user, password = smtp_pass, port = 25 }; | |
86 else | |
87 ok, err = smtp.send{ from = smtp_address, rcpt = rcpt, source = smtp.message(mesgt), | |
88 server = smtp_server, user = smtp_user, password = smtp_pass, port = smtp_port, create = enablessl }; | |
89 end | |
90 | |
91 if not ok then | |
92 module:log("error", "Failed to deliver to %s: %s", tostring(address), tostring(err)); | |
93 return; | |
94 end | |
95 return true; | |
96 end | |
97 | |
98 local vCard_mt = { | |
99 __index = function(t, k) | |
100 if type(k) ~= "string" then return nil end | |
101 for i=1,#t do | |
102 local t_i = rawget(t, i); | |
103 if t_i and t_i.name == k then | |
104 rawset(t, k, t_i); | |
105 return t_i; | |
106 end | |
107 end | |
108 end | |
109 }; | |
110 | |
111 local function get_user_vcard(user, host) | |
112 local vCard = dm_load(user, host or base_host, "vcard"); | |
113 if vCard then | |
114 vCard = st.deserialize(vCard); | |
115 vCard = vcard.from_xep54(vCard); | |
116 return setmetatable(vCard, vCard_mt); | |
117 end | |
118 end | |
119 | |
120 local changepass_tpl = get_template("changepass",".html"); | |
121 local sendmail_success_tpl = get_template("sendmailok",".html"); | |
122 local reset_success_tpl = get_template("resetok",".html"); | |
123 local token_tpl = get_template("token",".html"); | |
124 | |
125 function generate_page(event, display_options) | |
126 local request = event.request; | |
127 | |
128 return render(changepass_tpl, { | |
129 path = request.path; hostname = module.host; | |
130 notice = display_options and display_options.register_error or ""; | |
131 }) | |
132 end | |
133 | |
134 function generate_token_page(event, display_options) | |
135 local request = event.request; | |
136 | |
137 return render(token_tpl, { | |
138 path = request.path; hostname = module.host; | |
139 token = request.url.query; | |
140 notice = display_options and display_options.register_error or ""; | |
141 }) | |
142 end | |
143 | |
144 function generateToken(address) | |
145 math.randomseed(os.time()) | |
146 length = 16 | |
147 if length < 1 then return nil end | |
148 local array = {} | |
149 for i = 1, length, 2 do | |
150 array[i] = string.char(math.random(48,57)) | |
151 array[i+1] = string.char(math.random(97,122)) | |
152 end | |
153 local token = table.concat(array); | |
154 if not tokens_mails[token] then | |
155 | |
156 tokens_mails[token] = address; | |
157 tokens_expiration[token] = os.time(); | |
158 return token | |
159 else | |
160 module:log("error", "Reset password token collision: '%s'", token); | |
161 return generateToken(address) | |
162 end | |
163 end | |
164 | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
165 function isExpired(token) |
1342 | 166 if not tokens_expiration[token] then |
167 return nil; | |
168 end | |
169 if os.difftime(os.time(), tokens_expiration[token]) < 86400 then -- 86400 secs == 24h | |
170 -- token is valid yet | |
171 return nil; | |
172 else | |
173 -- token invalid, we can create a fresh one. | |
174 return true; | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
175 end |
1342 | 176 end |
177 | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
178 -- Expire tokens |
1342 | 179 expireTokens = function() |
180 for token,value in pairs(tokens_mails) do | |
181 if isExpired(token) then | |
182 module:log("info","Expiring password reset request from user '%s', not used.", tokens_mails[token]); | |
183 tokens_mails[token] = nil; | |
184 tokens_expiration[token] = nil; | |
185 end | |
186 end | |
187 return timer_repeat; | |
188 end | |
189 | |
190 -- Check if a user has a active token not used yet. | |
191 function hasTokenActive(address) | |
192 for token,value in pairs(tokens_mails) do | |
193 if address == value and not isExpired(token) then | |
194 return token; | |
195 end | |
196 end | |
197 return nil; | |
198 end | |
199 | |
200 function generateUrl(token) | |
201 local url; | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
202 |
1342 | 203 if https_host then |
204 url = "https://" .. https_host; | |
205 else | |
206 url = "http://" .. http_host; | |
207 end | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
208 |
1342 | 209 if https_port then |
210 url = url .. ":" .. https_port[1]; | |
211 else | |
212 url = url .. ":" .. http_port[1]; | |
213 end | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
214 |
1342 | 215 url = url .. url_path .. "token.html?" .. token; |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
216 |
1342 | 217 return url; |
218 end | |
219 | |
220 function sendMessage(jid, subject, message) | |
221 local msg = st.message({ from = module.host; to = jid; }): | |
222 tag("subject"):text(subject):up(): | |
223 tag("body"):text(message); | |
224 module:send(msg); | |
225 end | |
226 | |
227 function send_token_mail(form, origin) | |
1345
c60e9943dcb9
Fix problem handling form input
Luis G.F <luisgf@gmail.com>
parents:
1343
diff
changeset
|
228 local prepped_username = nodeprep(form.username); |
1342 | 229 local prepped_mail = form.email; |
1345
c60e9943dcb9
Fix problem handling form input
Luis G.F <luisgf@gmail.com>
parents:
1343
diff
changeset
|
230 local jid = prepped_username .. "@" .. module.host; |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
231 |
1342 | 232 if not prepped_username then |
233 return nil, "El usuario contiene caracteres incorrectos"; | |
234 end | |
235 if #prepped_username == 0 then | |
236 return nil, "El campo usuario está vacio"; | |
237 end | |
238 if not usermanager.user_exists(prepped_username, module.host) then | |
239 return nil, "El usuario NO existe"; | |
240 end | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
241 |
1342 | 242 if #prepped_mail == 0 then |
243 return nil, "El campo email está vacio"; | |
244 end | |
245 | |
246 local vcarduser = get_user_vcard(prepped_username, module.host); | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
247 |
1342 | 248 if not vcarduser then |
249 return nil, "User has not vCard"; | |
250 else | |
251 if not vcarduser.EMAIL then | |
252 return nil, "Esa cuente no tiene ningún email configurado en su vCard"; | |
253 end | |
254 | |
255 email = string.lower(vcarduser.EMAIL[1]); | |
256 | |
257 if email ~= string.lower(prepped_mail) then | |
258 return nil, "Dirección eMail incorrecta"; | |
259 end | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
260 |
1342 | 261 -- Check if has already a valid token, not used yet. |
262 if hasTokenActive(jid) then | |
263 local valid_until = tokens_expiration[hasTokenActive(jid)] + 86400; | |
264 return nil, "Ya tienes una petición de restablecimiento de clave válida hasta: " .. datetime.date(valid_until) .. " " .. datetime.time(valid_until); | |
265 end | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
266 |
1342 | 267 local url_token = generateToken(jid); |
268 local url = generateUrl(url_token); | |
269 local email_body = render(get_template("sendtoken",".mail"), {jid = jid, url = url} ); | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
270 |
1342 | 271 module:log("info", "Sending password reset mail to user %s", jid); |
272 send_email(email, smtp_address, email_body, mail_subject); | |
273 return "ok"; | |
274 end | |
275 | |
276 end | |
277 | |
278 function reset_password_with_token(form, origin) | |
279 local token = form.token; | |
280 local password = form.newpassword; | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
281 |
1342 | 282 if not token then |
283 return nil, "El Token es inválido"; | |
284 end | |
285 if not tokens_mails[token] then | |
286 return nil, "El Token no existe o ya fué usado"; | |
287 end | |
288 if not password then | |
289 return nil, "La campo clave no puede estar vacio"; | |
290 end | |
291 if #password < 5 then | |
292 return nil, "La clave debe tener una longitud de al menos 5 caracteres"; | |
293 end | |
294 local jid = tokens_mails[token]; | |
295 local user, host, resource = jidutil.split(jid); | |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
296 |
1342 | 297 usermanager.set_password(user, password, host); |
298 module:log("info", "Password changed with token for user %s", jid); | |
299 tokens_mails[token] = nil; | |
300 tokens_expiration[token] = nil; | |
301 sendMessage(jid, mail_subject, mail_body); | |
302 return "ok"; | |
303 end | |
304 | |
305 function generate_success(event, form) | |
306 return render(sendmail_success_tpl, { jid = nodeprep(form.username).."@"..module.host }); | |
307 end | |
308 | |
309 function generate_register_response(event, form, ok, err) | |
310 local message; | |
311 if ok then | |
312 return generate_success(event, form); | |
313 else | |
314 return generate_page(event, { register_error = err }); | |
315 end | |
316 end | |
317 | |
318 function handle_form_token(event) | |
319 local request, response = event.request, event.response; | |
320 local form = http.formdecode(request.body); | |
321 | |
322 local token_ok, token_err = send_token_mail(form, request); | |
323 response:send(generate_register_response(event, form, token_ok, token_err)); | |
324 | |
325 return true; -- Leave connection open until we respond above | |
326 end | |
327 | |
328 function generate_reset_success(event, form) | |
329 return render(reset_success_tpl, { }); | |
330 end | |
331 | |
332 function generate_reset_response(event, form, ok, err) | |
333 local message; | |
334 if ok then | |
335 return generate_reset_success(event, form); | |
336 else | |
337 return generate_token_page(event, { register_error = err }); | |
338 end | |
339 end | |
340 | |
341 function handle_form_reset(event) | |
342 local request, response = event.request, event.response; | |
343 local form = http.formdecode(request.body); | |
344 | |
345 local reset_ok, reset_err = reset_password_with_token(form, request); | |
346 response:send(generate_reset_response(event, form, reset_ok, reset_err)); | |
347 | |
348 return true; -- Leave connection open until we respond above | |
349 | |
350 end | |
351 | |
352 timer.add_task(timer_repeat, expireTokens); | |
353 | |
354 module:provides("http", { | |
355 default_path = url_path; | |
356 route = { | |
357 ["GET /style.css"] = render(get_template("style",".css"), {}); | |
358 ["GET /token.html"] = generate_token_page; | |
359 ["GET /"] = generate_page; | |
360 ["POST /token.html"] = handle_form_reset; | |
361 ["POST /"] = handle_form_token; | |
362 }; | |
363 }); | |
364 | |
365 |