diff mod_auth_oauth_external/mod_auth_oauth_external.lua @ 5653:62c6e17a5e9d

Merge
author Stephen Paul Weber <singpolyma@singpolyma.net>
date Mon, 18 Sep 2023 08:24:19 -0500
parents 4e79f344ae2f
children 0207fd248480
line wrap: on
line diff
--- a/mod_auth_oauth_external/mod_auth_oauth_external.lua	Mon Sep 18 08:22:07 2023 -0500
+++ b/mod_auth_oauth_external/mod_auth_oauth_external.lua	Mon Sep 18 08:24:19 2023 -0500
@@ -1,5 +1,6 @@
 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";
 
@@ -15,7 +16,8 @@
 -- 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???
 local client_id = module:get_option_string("oauth_external_client_id");
--- TODO -- local client_secret = module:get_option_string("oauth_external_client_secret");
+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" {
@@ -28,6 +30,32 @@
 local host = module.host;
 local provider = {};
 
+local function not_implemented()
+	return nil, "method not implemented"
+end
+
+-- With proper OAuth 2, most of these should be handled at the atuhorization
+-- server, no there.
+provider.test_password = not_implemented;
+provider.get_password = not_implemented;
+provider.set_password = not_implemented;
+provider.create_user = not_implemented;
+provider.delete_user = not_implemented;
+
+function provider.user_exists(_username)
+	-- Can this even be done in a generic way in OAuth 2?
+	-- OIDC and WebFinger perhaps?
+	return true;
+end
+
+function provider.users()
+	-- TODO this could be done by recording known users locally
+	return function ()
+		module:log("debug", "User iteration not supported");
+		return nil;
+	end
+end
+
 function provider.get_sasl_handler()
 	local profile = {};
 	profile.http_client = http.default; -- TODO configurable
@@ -35,14 +63,16 @@
 	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 = "openid";
+					scope = scope;
 				});
 			}))
 			if err or not (tok.code >= 200 and tok.code < 300) then
@@ -52,6 +82,12 @@
 			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;
+				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
@@ -61,36 +97,38 @@
 				return false, nil;
 			end
 			local response = json.decode(ret.body);
-			if type(response) ~= "table" or (response[username_field]) ~= username then
+			if type(response) ~= "table" then
+				return false, nil, nil;
+			elseif type(response[username_field]) ~= "string" 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.username = jid.escape(response[username_field]);
 			self.token_info = response;
 			return true, true;
 		end
 	end
-	function profile:oauthbearer(token)
-		if token == "" then
-			return false, nil, extra;
-		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;
+			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
+			local response = ret and 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
+
+			return jid.escape(response[username_field]), true, response;
 		end
-		local response = ret and 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
-
-		return response[username_field], true, response;
 	end
 	return sasl.new(host, profile);
 end