Mercurial > prosody-modules
view mod_email_pass/mod_email_pass.lua @ 5193:2bb29ece216b
mod_http_oauth2: Implement stateless dynamic client registration
Replaces previous explicit registration that required either the
additional module mod_adhoc_oauth2_client or manually editing the
database. That method was enough to have something to test with, but
would not probably not scale easily.
Dynamic client registration allows creating clients on the fly, which
may be even easier in theory.
In order to not allow basically unauthenticated writes to the database,
we implement a stateless model here.
per_host_key := HMAC(config -> oauth2_registration_key, hostname)
client_id := JWT { client metadata } signed with per_host_key
client_secret := HMAC(per_host_key, client_id)
This should ensure everything we need to know is part of the client_id,
allowing redirects etc to be validated, and the client_secret can be
validated with only the client_id and the per_host_key.
A nonce injected into the client_id JWT should ensure nobody can submit
the same client metadata and retrieve the same client_secret
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Fri, 03 Mar 2023 21:14:19 +0100 |
parents | c60e9943dcb9 |
children |
line wrap: on
line source
local dm_load = require "util.datamanager".load; local st = require "util.stanza"; local nodeprep = require "util.encodings".stringprep.nodeprep; local usermanager = require "core.usermanager"; local http = require "net.http"; local vcard = module:require "vcard"; local datetime = require "util.datetime"; local timer = require "util.timer"; local jidutil = require "util.jid"; -- SMTP related params. Readed from config local os_time = os.time; local smtp = require "socket.smtp"; local smtp_server = module:get_option_string("smtp_server", "localhost"); local smtp_port = module:get_option_string("smtp_port", "25"); local smtp_ssl = module:get_option_boolean("smtp_ssl", false); local smtp_user = module:get_option_string("smtp_username"); local smtp_pass = module:get_option_string("smtp_password"); local smtp_address = module:get_option("smtp_from") or ((smtp_user or "no-responder").."@"..(smtp_server or module.host)); local mail_subject = module:get_option_string("msg_subject") local mail_body = module:get_option_string("msg_body"); local url_path = module:get_option_string("url_path", "/resetpass"); -- This table has the tokens submited by the server tokens_mails = {}; tokens_expiration = {}; -- URL local https_host = module:get_option_string("https_host"); local http_host = module:get_option_string("http_host"); local https_port = module:get_option("https_ports", { 443 }); local http_port = module:get_option("http_ports", { 80 }); local timer_repeat = 120; -- repeat after 120 secs function enablessl() local sock = socket.tcp() return setmetatable({ connect = function(_, host, port) local r, e = sock:connect(host, port) if not r then return r, e end sock = ssl.wrap(sock, {mode='client', protocol='tlsv1'}) return sock:dohandshake() end }, { __index = function(t,n) return function(_, ...) return sock[n](sock, ...) end end }) end function template(data) -- Like util.template, but deals with plain text return { apply = function(values) return (data:gsub("{([^}]+)}", values)); end } end local function get_template(name, extension) local fh = assert(module:load_resource("templates/"..name..extension)); local data = assert(fh:read("*a")); fh:close(); return template(data); end local function render(template, data) return tostring(template.apply(data)); end function send_email(address, smtp_address, message_text, subject) local rcpt = "<"..address..">"; local mesgt = { headers = { to = address; subject = subject or ("Jabber password reset "..jid_bare(from_address)); }; body = message_text; }; local ok, err = nil; if not smtp_ssl then ok, err = smtp.send{ from = smtp_address, rcpt = rcpt, source = smtp.message(mesgt), server = smtp_server, user = smtp_user, password = smtp_pass, port = 25 }; else ok, err = smtp.send{ from = smtp_address, rcpt = rcpt, source = smtp.message(mesgt), server = smtp_server, user = smtp_user, password = smtp_pass, port = smtp_port, create = enablessl }; end if not ok then module:log("error", "Failed to deliver to %s: %s", tostring(address), tostring(err)); return; end return true; end local vCard_mt = { __index = function(t, k) if type(k) ~= "string" then return nil end for i=1,#t do local t_i = rawget(t, i); if t_i and t_i.name == k then rawset(t, k, t_i); return t_i; end end end }; local function get_user_vcard(user, host) local vCard = dm_load(user, host or base_host, "vcard"); if vCard then vCard = st.deserialize(vCard); vCard = vcard.from_xep54(vCard); return setmetatable(vCard, vCard_mt); end end local changepass_tpl = get_template("changepass",".html"); local sendmail_success_tpl = get_template("sendmailok",".html"); local reset_success_tpl = get_template("resetok",".html"); local token_tpl = get_template("token",".html"); function generate_page(event, display_options) local request = event.request; return render(changepass_tpl, { path = request.path; hostname = module.host; notice = display_options and display_options.register_error or ""; }) end function generate_token_page(event, display_options) local request = event.request; return render(token_tpl, { path = request.path; hostname = module.host; token = request.url.query; notice = display_options and display_options.register_error or ""; }) end function generateToken(address) math.randomseed(os.time()) length = 16 if length < 1 then return nil end local array = {} for i = 1, length, 2 do array[i] = string.char(math.random(48,57)) array[i+1] = string.char(math.random(97,122)) end local token = table.concat(array); if not tokens_mails[token] then tokens_mails[token] = address; tokens_expiration[token] = os.time(); return token else module:log("error", "Reset password token collision: '%s'", token); return generateToken(address) end end function isExpired(token) if not tokens_expiration[token] then return nil; end if os.difftime(os.time(), tokens_expiration[token]) < 86400 then -- 86400 secs == 24h -- token is valid yet return nil; else -- token invalid, we can create a fresh one. return true; end end -- Expire tokens expireTokens = function() for token,value in pairs(tokens_mails) do if isExpired(token) then module:log("info","Expiring password reset request from user '%s', not used.", tokens_mails[token]); tokens_mails[token] = nil; tokens_expiration[token] = nil; end end return timer_repeat; end -- Check if a user has a active token not used yet. function hasTokenActive(address) for token,value in pairs(tokens_mails) do if address == value and not isExpired(token) then return token; end end return nil; end function generateUrl(token) local url; if https_host then url = "https://" .. https_host; else url = "http://" .. http_host; end if https_port then url = url .. ":" .. https_port[1]; else url = url .. ":" .. http_port[1]; end url = url .. url_path .. "token.html?" .. token; return url; end function sendMessage(jid, subject, message) local msg = st.message({ from = module.host; to = jid; }): tag("subject"):text(subject):up(): tag("body"):text(message); module:send(msg); end function send_token_mail(form, origin) local prepped_username = nodeprep(form.username); local prepped_mail = form.email; local jid = prepped_username .. "@" .. module.host; if not prepped_username then return nil, "El usuario contiene caracteres incorrectos"; end if #prepped_username == 0 then return nil, "El campo usuario está vacio"; end if not usermanager.user_exists(prepped_username, module.host) then return nil, "El usuario NO existe"; end if #prepped_mail == 0 then return nil, "El campo email está vacio"; end local vcarduser = get_user_vcard(prepped_username, module.host); if not vcarduser then return nil, "User has not vCard"; else if not vcarduser.EMAIL then return nil, "Esa cuente no tiene ningún email configurado en su vCard"; end email = string.lower(vcarduser.EMAIL[1]); if email ~= string.lower(prepped_mail) then return nil, "Dirección eMail incorrecta"; end -- Check if has already a valid token, not used yet. if hasTokenActive(jid) then local valid_until = tokens_expiration[hasTokenActive(jid)] + 86400; return nil, "Ya tienes una petición de restablecimiento de clave válida hasta: " .. datetime.date(valid_until) .. " " .. datetime.time(valid_until); end local url_token = generateToken(jid); local url = generateUrl(url_token); local email_body = render(get_template("sendtoken",".mail"), {jid = jid, url = url} ); module:log("info", "Sending password reset mail to user %s", jid); send_email(email, smtp_address, email_body, mail_subject); return "ok"; end end function reset_password_with_token(form, origin) local token = form.token; local password = form.newpassword; if not token then return nil, "El Token es inválido"; end if not tokens_mails[token] then return nil, "El Token no existe o ya fué usado"; end if not password then return nil, "La campo clave no puede estar vacio"; end if #password < 5 then return nil, "La clave debe tener una longitud de al menos 5 caracteres"; end local jid = tokens_mails[token]; local user, host, resource = jidutil.split(jid); usermanager.set_password(user, password, host); module:log("info", "Password changed with token for user %s", jid); tokens_mails[token] = nil; tokens_expiration[token] = nil; sendMessage(jid, mail_subject, mail_body); return "ok"; end function generate_success(event, form) return render(sendmail_success_tpl, { jid = nodeprep(form.username).."@"..module.host }); end function generate_register_response(event, form, ok, err) local message; if ok then return generate_success(event, form); else return generate_page(event, { register_error = err }); end end function handle_form_token(event) local request, response = event.request, event.response; local form = http.formdecode(request.body); local token_ok, token_err = send_token_mail(form, request); response:send(generate_register_response(event, form, token_ok, token_err)); return true; -- Leave connection open until we respond above end function generate_reset_success(event, form) return render(reset_success_tpl, { }); end function generate_reset_response(event, form, ok, err) local message; if ok then return generate_reset_success(event, form); else return generate_token_page(event, { register_error = err }); end end function handle_form_reset(event) local request, response = event.request, event.response; local form = http.formdecode(request.body); local reset_ok, reset_err = reset_password_with_token(form, request); response:send(generate_reset_response(event, form, reset_ok, reset_err)); return true; -- Leave connection open until we respond above end timer.add_task(timer_repeat, expireTokens); module:provides("http", { default_path = url_path; route = { ["GET /style.css"] = render(get_template("style",".css"), {}); ["GET /token.html"] = generate_token_page; ["GET /"] = generate_page; ["POST /token.html"] = handle_form_reset; ["POST /"] = handle_form_token; }; });