changeset 5926:9bcc26406b47

mod_rest: Return specific errors from credential checks This is a step towards returning details of what went wrong when checking credentials, distinguishing missing, malformed, and wrong credentials.
author Kim Alvefur <zash@zash.se>
date Sat, 06 Jul 2024 19:26:07 +0200
parents 32d1abb89dfe
children 07f32bf41303
files mod_rest/mod_rest.lua
diffstat 1 files changed, 35 insertions(+), 26 deletions(-) [+]
line wrap: on
line diff
--- a/mod_rest/mod_rest.lua	Thu Jul 04 16:06:32 2024 +0200
+++ b/mod_rest/mod_rest.lua	Sat Jul 06 19:26:07 2024 +0200
@@ -34,35 +34,58 @@
 	www_authenticate_header = table.concat(header, ", ");
 end
 
-local function check_credentials(request)
+local post_errors = errors.init("mod_rest", {
+	noauthz = { code = 401; type = "auth"; condition = "not-authorized"; text = "No credentials provided" };
+	unauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials not accepted" };
+	malformauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials malformed" };
+	prepauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials failed stringprep" };
+	parse = { code = 400; type = "modify"; condition = "not-well-formed"; text = "Failed to parse payload" };
+	xmlns = { code = 422; type = "modify"; condition = "invalid-namespace"; text = "'xmlns' attribute must be empty" };
+	name = { code = 422; type = "modify"; condition = "unsupported-stanza-type"; text = "Invalid stanza, must be 'message', 'presence' or 'iq'." };
+	to = { code = 422; type = "modify"; condition = "improper-addressing"; text = "Invalid destination JID" };
+	from = { code = 422; type = "modify"; condition = "invalid-from"; text = "Invalid source JID" };
+	from_auth = { code = 403; type = "auth"; condition = "not-authorized"; text = "Not authorized to send stanza with requested 'from'" };
+	iq_type = { code = 422; type = "modify"; condition = "invalid-xml"; text = "'iq' stanza must be of type 'get' or 'set'" };
+	iq_tags = { code = 422; type = "modify"; condition = "bad-format"; text = "'iq' stanza must have exactly one child tag" };
+	mediatype = { code = 415; type = "cancel"; condition = "bad-format"; text = "Unsupported media type" };
+	size = { code = 413; type = "modify"; condition = "resource-constraint", text = "Payload too large" };
+});
+
+local function check_credentials(request) -- > session | boolean, error
 	local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$");
 	if not (auth_type and auth_data) or not auth_mechanisms:contains(auth_type) then
-		return false;
+		return nil, post_errors.new("noauthz", { request = request });
 	end
 
 	if auth_type == "Basic" then
 		local creds = base64.decode(auth_data);
-		if not creds then return false; end
+		if not creds then
+			return nil, post_errors.new("malformauthz", { request = request });
+		end
 		local username, password = string.match(creds, "^([^:]+):(.*)$");
-		if not username then return false; end
+		if not username then
+			return nil, post_errors.new("malformauthz", { request = request });
+		end
 		username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password);
-		if not username or not password then return false; end
+		if not username or not password then
+			return false, post_errors.new("prepauthz", { request = request });
+		end
 		if not um.test_password(username, module.host, password) then
-			return false;
+			return false, post_errors.new("unauthz", { request = request });
 		end
-		return { username = username, host = module.host };
+		return { username = username; host = module.host };
 	elseif auth_type == "Bearer" then
 		if tokens.get_token_session then
 			return tokens.get_token_session(auth_data);
 		else -- COMPAT w/0.12
 			local token_info = tokens.get_token_info(auth_data);
 			if not token_info or not token_info.session then
-				return false;
+				return false, post_errors.new("unauthz", { request = request });
 			end
 			return token_info.session;
 		end
 	end
-	return nil;
+	return nil, post_errors.new("noauthz", { request = request });
 end
 
 if module:get_option_string("authentication") == "anonymous" and module:get_option_boolean("anonymous_rest") then
@@ -268,21 +291,6 @@
 	error "unsupported encoding";
 end
 
-local post_errors = errors.init("mod_rest", {
-	noauthz = { code = 401; type = "auth"; condition = "not-authorized"; text = "No credentials provided" };
-	unauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials not accepted" };
-	parse = { code = 400; type = "modify"; condition = "not-well-formed"; text = "Failed to parse payload" };
-	xmlns = { code = 422; type = "modify"; condition = "invalid-namespace"; text = "'xmlns' attribute must be empty" };
-	name = { code = 422; type = "modify"; condition = "unsupported-stanza-type"; text = "Invalid stanza, must be 'message', 'presence' or 'iq'." };
-	to = { code = 422; type = "modify"; condition = "improper-addressing"; text = "Invalid destination JID" };
-	from = { code = 422; type = "modify"; condition = "invalid-from"; text = "Invalid source JID" };
-	from_auth = { code = 403; type = "auth"; condition = "not-authorized"; text = "Not authorized to send stanza with requested 'from'" };
-	iq_type = { code = 422; type = "modify"; condition = "invalid-xml"; text = "'iq' stanza must be of type 'get' or 'set'" };
-	iq_tags = { code = 422; type = "modify"; condition = "bad-format"; text = "'iq' stanza must have exactly one child tag" };
-	mediatype = { code = 415; type = "cancel"; condition = "bad-format"; text = "Unsupported media type" };
-	size = { code = 413; type = "modify"; condition = "resource-constraint", text = "Payload too large" };
-});
-
 -- GET → iq-get
 local function parse_request(request, path)
 	if path and request.method == "GET" then
@@ -308,9 +316,10 @@
 		response.headers.www_authenticate = www_authenticate_header;
 		return post_errors.new("noauthz");
 	else
-		origin = check_credentials(request);
+		local err;
+		origin, err = check_credentials(request);
 		if not origin then
-			return post_errors.new("unauthz");
+			return err or post_errors.new("unauthz");
 		end
 		from = jid.join(origin.username, origin.host, origin.resource);
 		origin.full_jid = from;