Mercurial > prosody-modules
view mod_sasl2/mod_sasl2.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 | e9cf361982d5 |
children | 828e5e443613 |
line wrap: on
line source
-- Prosody IM -- Copyright (C) 2019 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- XEP-0388: Extensible SASL Profile -- local st = require "util.stanza"; local errors = require "util.error"; local base64 = require "util.encodings".base64; local jid_join = require "util.jid".join; local set = require "util.set"; local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; local sm_make_authenticated = require "core.sessionmanager".make_authenticated; local xmlns_sasl2 = "urn:xmpp:sasl:2"; local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true)); local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false) local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"}); local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" }); local host = module.host; local function tls_unique(self) return self.userdata["tls-unique"]:ssl_peerfinished(); end local function tls_exporter(conn) if not conn.ssl_exportkeyingmaterial then return end return conn:ssl_exportkeyingmaterial("EXPORTER-Channel-Binding", 32, ""); end local function sasl_tls_exporter(self) return tls_exporter(self.userdata["tls-exporter"]); end module:hook("stream-features", function(event) local origin, features = event.origin, event.features; local log = origin.log or module._log; if origin.type ~= "c2s_unauthed" then log("debug", "Already authenticated"); return elseif secure_auth_only and not origin.secure then log("debug", "Not offering authentication on insecure connection"); return; end local sasl_handler = usermanager_get_sasl_handler(host, origin) origin.sasl_handler = sasl_handler; local channel_bindings = set.new() if origin.encrypted then -- check whether LuaSec has the nifty binding to the function needed for tls-unique -- FIXME: would be nice to have this check only once and not for every socket if sasl_handler.add_cb_handler then local info = origin.conn:ssl_info(); if info and info.protocol == "TLSv1.3" then log("debug", "Channel binding 'tls-unique' undefined in context of TLS 1.3"); if tls_exporter(origin.conn) then log("debug", "Channel binding 'tls-exporter' supported"); sasl_handler:add_cb_handler("tls-exporter", sasl_tls_exporter); channel_bindings:add("tls-exporter"); end elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then log("debug", "Channel binding 'tls-unique' supported"); sasl_handler:add_cb_handler("tls-unique", tls_unique); channel_bindings:add("tls-unique"); else log("debug", "Channel binding 'tls-unique' not supported (by LuaSec?)"); end sasl_handler["userdata"] = { ["tls-unique"] = origin.conn; ["tls-exporter"] = origin.conn; }; else log("debug", "Channel binding not supported by SASL handler"); end end local mechanisms = st.stanza("authentication", { xmlns = xmlns_sasl2 }); local available_mechanisms = sasl_handler:mechanisms() for mechanism in pairs(available_mechanisms) do if disabled_mechanisms:contains(mechanism) then log("debug", "Not offering disabled mechanism %s", mechanism); elseif not origin.secure and insecure_mechanisms:contains(mechanism) then log("debug", "Not offering mechanism %s on insecure connection", mechanism); else log("debug", "Offering mechanism %s", mechanism); mechanisms:text_tag("mechanism", mechanism); end end features:add_direct_child(mechanisms); local inline = st.stanza("inline"); module:fire_event("advertise-sasl-features", { origin = origin, features = inline, stream = event.stream }); mechanisms:add_direct_child(inline); end, 1); local function handle_status(session, status, ret, err_msg) local err = nil; if status == "error" then ret, err = nil, ret; if not errors.is_err(err) then err = errors.new({ condition = err, text = err_msg }, { session = session }); end end return module:fire_event("sasl2/"..session.base_type.."/"..status, { session = session, message = ret; error = err; error_text = err_msg; }); end module:hook("sasl2/c2s/failure", function (event) local session, condition, text = event.session, event.message, event.error_text; local failure = st.stanza("failure", { xmlns = xmlns_sasl2 }) :tag(condition, { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl" }):up(); if text then failure:text_tag("text", text); end session.send(failure); return true; end); module:hook("sasl2/c2s/error", function (event) local session = event.session session.send(st.stanza("failure", { xmlns = xmlns_sasl2 }) :tag(event.error and event.error.condition)); return true; end); module:hook("sasl2/c2s/challenge", function (event) local session = event.session; session.send(st.stanza("challenge", { xmlns = xmlns_sasl2 }) :text(base64.encode(event.message))); return true; end); module:hook("sasl2/c2s/success", function (event) local session = event.session local ok, err = sm_make_authenticated(session, session.sasl_handler.username); if not ok then handle_status(session, "failure", err); return true; end event.success = st.stanza("success", { xmlns = xmlns_sasl2 }); if event.message then event.success:text_tag("additional-data", base64.encode(event.message)); end end, 1000); module:hook("sasl2/c2s/success", function (event) local session = event.session event.success:text_tag("authorization-identifier", jid_join(session.username, session.host, session.resource)); session.send(event.success); end, -1000); module:hook("sasl2/c2s/success", function (event) local session = event.session; local features = st.stanza("stream:features"); module:fire_event("stream-features", { origin = session, features = features }); session.send(features); end, -1500); -- The gap here is to allow modules to do stuff to the stream after the stanza -- is sent, but before we proceed with anything else. This is expected to be -- a common pattern with SASL2, which allows atomic negotiation of a bunch of -- stream features. module:hook("sasl2/c2s/success", function (event) --luacheck: ignore 212/event event.session.sasl_handler = nil; return true; end, -2000); local function process_cdata(session, cdata) if cdata then cdata = base64.decode(cdata); if not cdata then return handle_status(session, "failure", "incorrect-encoding"); end end return handle_status(session, session.sasl_handler:process(cdata)); end module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) if secure_auth_only and not session.secure then return handle_status(session, "failure", "encryption-required"); end local sasl_handler = session.sasl_handler; if not sasl_handler then sasl_handler = usermanager_get_sasl_handler(host, session); session.sasl_handler = sasl_handler; end local mechanism = assert(auth.attr.mechanism); if not sasl_handler:select(mechanism) then return handle_status(session, "failure", "invalid-mechanism"); end local user_agent = auth:get_child("user-agent"); if user_agent then session.client_id = user_agent.attr.id; end local initial = auth:get_child_text("initial-response"); return process_cdata(session, initial); end); module:hook_tag(xmlns_sasl2, "response", function (session, response) local sasl_handler = session.sasl_handler; if not sasl_handler or not sasl_handler.selected then return handle_status(session, "failure", "invalid-mechanism"); end return process_cdata(session, response:get_text()); end);