Mercurial > prosody-modules
view mod_vjud/mod_vjud.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 | 5dffb85e62c4 |
children |
line wrap: on
line source
local dm_load = require "util.datamanager".load; local dm_store = require "util.datamanager".store; local usermanager = require "core.usermanager"; local dataforms_new = require "util.dataforms".new; local jid_split = require "util.jid".prepped_split; local vcard = module:require "vcard"; local rawget, rawset = rawget, rawset; local s_lower = string.lower; local s_find = string.find; local st = require "util.stanza"; local template = require "util.template"; local instructions = module:get_option_string("vjud_instructions", "Fill in one or more fields to search for any matching Jabber users."); local get_reply = template[[ <query xmlns="jabber:iq:search"> <instructions>{instructions}</instructions> <first/> <last/> <nick/> <email/> </query> ]].apply({ instructions = instructions }); local item_template = template[[ <item xmlns="jabber:iq:search" jid="{jid}"> <first>{first}</first> <last>{last}</last> <nick>{nick}</nick> <email>{email}</email> </item> ]]; local search_mode = module:get_option_string("vjud_mode", "opt-in"); local allow_remote = module:get_option_boolean("allow_remote_searches", search_mode ~= "all"); local base_host = module:get_option_string("vjud_search_domain", module:get_host_type() == "component" and module.host:gsub("^[^.]+%.","") or module.host); module:depends"disco"; if module:get_host_type() == "component" then module:add_identity("directory", "user", module:get_option_string("name", "User search")); end module:add_feature("jabber:iq:search"); 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, err = dm_load(user, host or base_host, "vcard"); if not vCard then return nil, err; end vCard = st.deserialize(vCard); vCard, err = vcard.from_xep54(vCard); if not vCard then return nil, err; end return setmetatable(vCard, vCard_mt); end local at_host = "@"..base_host; local users; -- The user iterator module:hook("iq/host/jabber:iq:search:query", function(event) local origin, stanza = event.origin, event.stanza; if not (allow_remote or origin.type == "c2s") then origin.send(st.error_reply(stanza, "cancel", "not-allowed")) return true; end if stanza.attr.type == "get" then origin.send(st.reply(stanza):add_child(get_reply)); else -- type == "set" local query = stanza.tags[1]; local first, last, nick, email = s_lower(query:get_child_text"first" or ""), s_lower(query:get_child_text"last" or ""), s_lower(query:get_child_text"nick" or ""), s_lower(query:get_child_text"email" or ""); first = #first >= 2 and first; last = #last >= 2 and last; nick = #nick >= 2 and nick; email = #email >= 2 and email; if not ( first or last or nick or email ) then origin.send(st.error_reply(stanza, "modify", "not-acceptable", "All fields were empty or too short")); return true; end local reply = st.reply(stanza):query("jabber:iq:search"); local username, hostname = jid_split(email); if hostname == base_host and username and usermanager.user_exists(username, hostname) then local vCard, err = get_user_vcard(username); if not vCard then module:log("debug", "Couldn't get vCard for user %s: %s", username, err or "unknown error"); else reply:add_child(item_template.apply{ jid = username..at_host; first = vCard.N and vCard.N[2] or nil; last = vCard.N and vCard.N[1] or nil; nick = vCard.NICKNAME and vCard.NICKNAME[1] or username; email = vCard.EMAIL and vCard.EMAIL[1] or nil; }); end else for username in users() do local vCard = get_user_vcard(username); if vCard and ((first and vCard.N and s_find(s_lower(vCard.N[2]), first, nil, true)) or (last and vCard.N and s_find(s_lower(vCard.N[1]), last, nil, true)) or (nick and vCard.NICKNAME and s_find(s_lower(vCard.NICKNAME[1]), nick, nil, true)) or (email and vCard.EMAIL and s_find(s_lower(vCard.EMAIL[1]), email, nil, true))) then reply:add_child(item_template.apply{ jid = username..at_host; first = vCard.N and vCard.N[2] or nil; last = vCard.N and vCard.N[1] or nil; nick = vCard.NICKNAME and vCard.NICKNAME[1] or username; email = vCard.EMAIL and vCard.EMAIL[1] or nil; }); end end end origin.send(reply); end return true; end); if search_mode == "all" then function users() return usermanager.users(base_host); end else -- if "opt-in", default local opted_in; function module.load() opted_in = dm_load(nil, module.host, "user_index") or {}; end function module.unload() dm_store(nil, module.host, "user_index", opted_in); end function users() return pairs(opted_in); end local opt_in_layout = dataforms_new{ title = "Search settings"; instructions = "Do you want to appear in search results?"; { name = "searchable", label = "Appear in search results?", type = "boolean", }, }; local function opt_in_handler(self, data, state) local username, hostname = jid_split(data.from); if state then -- the second return value if data.action == "cancel" then return { status = "canceled" }; end if not username or not hostname or hostname ~= base_host then return { status = "error", error = { type = "cancel", condition = "forbidden", message = "Invalid user or hostname." } }; end local fields = opt_in_layout:data(data.form); opted_in[username] = fields.searchable or nil return { status = "completed" } else -- No state, send the form. return { status = "executing", actions = { "complete" }, form = { layout = opt_in_layout, values = { searchable = opted_in[username] } } }, true; end end local adhoc_new = module:require "adhoc".new; local adhoc_vjudsetup = adhoc_new("Search settings", "vjudsetup", opt_in_handler, "any");--, "self");-- and nil); module:depends"adhoc"; module:provides("adhoc", adhoc_vjudsetup); end