Mercurial > prosody-modules
view mod_auth_oauth_external/mod_auth_oauth_external.lua @ 6534:5aeefc2251d4
mod_firewall: Load a script completely or not at all
Previously it was possible that some chains would load, but others would not.
This can be surprising (that a "half-loaded" state is possible) and hard to
debug.
This change makes it so that if some chains don't compile, the whole script
will be treated as not loaded.
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Wed, 06 May 2026 16:15:38 +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);
