Software /
code /
prosody-modules
Changeset
5345:3390bb2f9f6c
mod_auth_oauth_external: Support PLAIN via resource owner password grant
Might not be supported by the backend but PLAIN is the lowest common
denominator, so not having it would lock out a lot of clients.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Thu, 16 Mar 2023 12:45:52 +0100 |
parents | 5344:0a6d2b79a8bf |
children | 5346:d9bc8712a745 |
files | mod_auth_oauth_external/README.md mod_auth_oauth_external/mod_auth_oauth_external.lua |
diffstat | 2 files changed, 62 insertions(+), 2 deletions(-) [+] |
line wrap: on
line diff
--- a/mod_auth_oauth_external/README.md Thu Mar 16 12:45:22 2023 +0100 +++ b/mod_auth_oauth_external/README.md Thu Mar 16 12:45:52 2023 +0100 @@ -7,7 +7,7 @@ This module provides external authentication via an external [AOuth 2](https://datatracker.ietf.org/doc/html/rfc7628) authorization server and supports the [SASL OAUTHBEARER authentication][rfc7628] -mechanism. +mechanism as well as PLAIN for legacy clients (this is all of them). # How it works @@ -15,6 +15,9 @@ the Authorization server to validate them, returning info about the user back to Prosody. +Alternatively for legacy clients, Prosody receives the users username +and password and retrieves a token itself, then proceeds as above. + # Configuration `oauth_external_discovery_url` @@ -35,6 +38,21 @@ structure returned by the validation endpoint that contains the XMPP localpart. +## For SASL PLAIN + +`oauth_external_resource_owner_password` +: Boolean. Defaults to `true`. Whether to allow the *insecure* + resource owner password grant and SASL PLAIN. + +`oauth_external_token_endpoint` +: URL string. OAuth 2 [Token + Endpoint](https://www.rfc-editor.org/rfc/rfc6749#section-3.2) used + to retrieve token in order to then retrieve the username. + +`oauth_external_client_id` +: String. Client ID used to identify Prosody during the resource owner + password grant. + # Compatibility Version Status
--- a/mod_auth_oauth_external/mod_auth_oauth_external.lua Thu Mar 16 12:45:22 2023 +0100 +++ b/mod_auth_oauth_external/mod_auth_oauth_external.lua Thu Mar 16 12:45:52 2023 +0100 @@ -6,12 +6,14 @@ -- TODO -- local issuer_identity = module:get_option_string("oauth_external_issuer"); local oidc_discovery_url = module:get_option_string("oauth_external_discovery_url") local validation_endpoint = module:get_option_string("oauth_external_validation_endpoint"); +local token_endpoint = module:get_option_string("oauth_external_token_endpoint"); local username_field = module:get_option_string("oauth_external_username_field", "preferred_username"); +local allow_plain = module:get_option_boolean("oauth_external_resource_owner_password", true); -- XXX Hold up, does whatever done here even need any of these things? Are we -- the OAuth client? Is the XMPP client the OAuth client? What are we??? --- TODO -- local client_id = module:get_option_string("oauth_external_client_id"); +local client_id = module:get_option_string("oauth_external_client_id"); -- TODO -- local client_secret = module:get_option_string("oauth_external_client_secret"); --[[ More or less required endpoints @@ -29,6 +31,46 @@ local profile = {}; profile.http_client = http.default; -- 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) + 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; + username = map_username(username, realm); + password = password; + scope = "openid"; + }); + })) + 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 + 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 (response[username_field]) ~= username then + return false, nil, nil; + end + if response.jid then + self.username, self.realm, self.resource = jid.prepped_split(response.jid, true); + end + self.role = response.role; + self.token_info = response; + return true, true; + end + end function profile:oauthbearer(token) if token == "" then return false, nil, extra;