Mercurial > prosody-modules
view mod_auth_oauth_external/mod_auth_oauth_external.lua @ 6556:7477e97a9045
mod_firewall: Apply pre-reload state before re-reading config
This change makes load/reload a bit more robust. module.load() runs before
module.restore() and it reads from the config and updates the state (if
needed).
However, after this, module.restore() could run and apply the old state again.
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Sun, 24 May 2026 20:03:20 +0100 |
| parents | 6241b2f5e92f |
| children |
line wrap: on
line source
local http = require "net.http"; local async = require "util.async"; local jid = require "util.jid"; local json = require "util.json"; local sasl = require "util.sasl"; -- Details about the OAuth2 Issuer local issuer_identity = module:get_option_string("oauth_external_issuer"); local oidc_discovery_url = module:get_option_string("oauth_external_discovery_url", issuer_identity and issuer_identity .. "/.well-known/oauth-authorization-server" or nil); -- OAuth2 endpoints -- These could be discovered dynamically if unset local validation_endpoint = module:get_option_string("oauth_external_validation_endpoint"); local token_endpoint = module:get_option_string("oauth_external_token_endpoint"); -- Field used to decide the XMPP address localpart local username_field = module:get_option_string("oauth_external_username_field", "preferred_username"); -- Settings needed for Resource Owner Password Grant, where Prosody acts as an OAuth2 Client -- Since at this time, no clients support SASL OAUTHBEARER, this is the only way this works local allow_plain = module:get_option_boolean("oauth_external_resource_owner_password", true); local client_id = module:get_option_string("oauth_external_client_id"); local client_secret = module:get_option_string("oauth_external_client_secret"); local scope = module:get_option_string("oauth_external_scope", "openid"); --[[ More or less required endpoints digraph "oauth endpoints" { issuer -> discovery -> { registration validation } registration -> { client_id client_secret } { client_id client_secret validation } -> required } --]] assert(validation_endpoint or token_endpoint, "One or both of 'oauth_external_validation_endpoint' and 'oauth_external_token_endpoint' must be configured"); local accounts = module:open_store("accounts"); local provider = {}; -- TODO remove, not needed after https://hg.prosody.im/trunk/rev/01f95f3de6fc local function not_implemented() return nil, "method not implemented" end -- With proper OAuth 2, most of these should be handled at the authorization -- server, no there. provider.test_password = not_implemented; provider.get_password = not_implemented; provider.set_password = not_implemented; provider.create_user = not_implemented; function provider.delete_user(username) return accounts:set(username, nil); end function provider.get_account_info(username) local account, err = accounts:get(username); if not account then return nil, err or "Account not available"; end return { created = account.created; password_updated = account.updated; enabled = not account.disabled; }; end function provider.user_exists(username) local account, err = accounts:get(username); if err then return nil, err; elseif account then return true; else return false end end function provider.users() return accounts:users(); end function provider.get_sasl_handler() local profile = {}; profile.http_client = http.default:new({ connection_pooling = true }); -- TODO configurable local extra = { oidc_discovery_url = oidc_discovery_url }; if token_endpoint and allow_plain then local map_username = function (username, _realm) return username; end; --jid.join; -- TODO configurable function profile:plain_test(username, password, realm) username = jid.unescape(username); -- COMPAT Mastodon local tok, err = async.wait_for(self.profile.http_client:request(token_endpoint, { headers = { ["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"; ["Accept"] = "application/json" }; body = http.formencode({ grant_type = "password"; client_id = client_id; client_secret = client_secret; username = map_username(username, realm); password = password; scope = scope; }); })) if err or not (tok.code >= 200 and tok.code < 300) then return false, nil; end local token_resp = json.decode(tok.body); if not token_resp or string.lower(token_resp.token_type or "") ~= "bearer" then return false, nil; end if not validation_endpoint then -- We're not going to get more info, only the username self.username = jid.escape(username); self.token_info = token_resp; accounts:set(self.username, { exists = true }); return true, true; end local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint, { headers = { ["Authorization"] = "Bearer " .. token_resp.access_token; ["Accept"] = "application/json" } })); if err then return false, nil; end if not (ret.code >= 200 and ret.code < 300) then return false, nil; end local response = json.decode(ret.body); if type(response) ~= "table" or type(response[username_field]) ~= "string" then return false, nil, nil; end self.token_info = response; self.username = jid.escape(response[username_field]); accounts:set(self.username, { exists = true }); return true, true; end end if validation_endpoint then function profile:oauthbearer(token) if token == "" then return false, nil, extra; end local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint, { headers = { ["Authorization"] = "Bearer " .. token; ["Accept"] = "application/json" }; })); if err then return false, nil, extra; end -- response decoded earlier since it can include an error struct that should be returned to client local response = json.decode(ret.body); if not (ret.code >= 200 and ret.code < 300) then return false, nil, response or extra; end if type(response) ~= "table" or type(response[username_field]) ~= "string" then return false, nil, nil; end self.token_info = response; return jid.escape(response[username_field]), true, response; end end return sasl.new(module.host, profile); end module:provides("auth", provider);
