comparison mod_rest/mod_rest.lua @ 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 d5e6617e47cc
comparison
equal deleted inserted replaced
5925:32d1abb89dfe 5926:9bcc26406b47
32 header[#header+1] = ("%s realm=%q"):format(mech, realm); 32 header[#header+1] = ("%s realm=%q"):format(mech, realm);
33 end 33 end
34 www_authenticate_header = table.concat(header, ", "); 34 www_authenticate_header = table.concat(header, ", ");
35 end 35 end
36 36
37 local function check_credentials(request) 37 local post_errors = errors.init("mod_rest", {
38 noauthz = { code = 401; type = "auth"; condition = "not-authorized"; text = "No credentials provided" };
39 unauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials not accepted" };
40 malformauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials malformed" };
41 prepauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials failed stringprep" };
42 parse = { code = 400; type = "modify"; condition = "not-well-formed"; text = "Failed to parse payload" };
43 xmlns = { code = 422; type = "modify"; condition = "invalid-namespace"; text = "'xmlns' attribute must be empty" };
44 name = { code = 422; type = "modify"; condition = "unsupported-stanza-type"; text = "Invalid stanza, must be 'message', 'presence' or 'iq'." };
45 to = { code = 422; type = "modify"; condition = "improper-addressing"; text = "Invalid destination JID" };
46 from = { code = 422; type = "modify"; condition = "invalid-from"; text = "Invalid source JID" };
47 from_auth = { code = 403; type = "auth"; condition = "not-authorized"; text = "Not authorized to send stanza with requested 'from'" };
48 iq_type = { code = 422; type = "modify"; condition = "invalid-xml"; text = "'iq' stanza must be of type 'get' or 'set'" };
49 iq_tags = { code = 422; type = "modify"; condition = "bad-format"; text = "'iq' stanza must have exactly one child tag" };
50 mediatype = { code = 415; type = "cancel"; condition = "bad-format"; text = "Unsupported media type" };
51 size = { code = 413; type = "modify"; condition = "resource-constraint", text = "Payload too large" };
52 });
53
54 local function check_credentials(request) -- > session | boolean, error
38 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$"); 55 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$");
39 if not (auth_type and auth_data) or not auth_mechanisms:contains(auth_type) then 56 if not (auth_type and auth_data) or not auth_mechanisms:contains(auth_type) then
40 return false; 57 return nil, post_errors.new("noauthz", { request = request });
41 end 58 end
42 59
43 if auth_type == "Basic" then 60 if auth_type == "Basic" then
44 local creds = base64.decode(auth_data); 61 local creds = base64.decode(auth_data);
45 if not creds then return false; end 62 if not creds then
63 return nil, post_errors.new("malformauthz", { request = request });
64 end
46 local username, password = string.match(creds, "^([^:]+):(.*)$"); 65 local username, password = string.match(creds, "^([^:]+):(.*)$");
47 if not username then return false; end 66 if not username then
67 return nil, post_errors.new("malformauthz", { request = request });
68 end
48 username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password); 69 username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password);
49 if not username or not password then return false; end 70 if not username or not password then
71 return false, post_errors.new("prepauthz", { request = request });
72 end
50 if not um.test_password(username, module.host, password) then 73 if not um.test_password(username, module.host, password) then
51 return false; 74 return false, post_errors.new("unauthz", { request = request });
52 end 75 end
53 return { username = username, host = module.host }; 76 return { username = username; host = module.host };
54 elseif auth_type == "Bearer" then 77 elseif auth_type == "Bearer" then
55 if tokens.get_token_session then 78 if tokens.get_token_session then
56 return tokens.get_token_session(auth_data); 79 return tokens.get_token_session(auth_data);
57 else -- COMPAT w/0.12 80 else -- COMPAT w/0.12
58 local token_info = tokens.get_token_info(auth_data); 81 local token_info = tokens.get_token_info(auth_data);
59 if not token_info or not token_info.session then 82 if not token_info or not token_info.session then
60 return false; 83 return false, post_errors.new("unauthz", { request = request });
61 end 84 end
62 return token_info.session; 85 return token_info.session;
63 end 86 end
64 end 87 end
65 return nil; 88 return nil, post_errors.new("noauthz", { request = request });
66 end 89 end
67 90
68 if module:get_option_string("authentication") == "anonymous" and module:get_option_boolean("anonymous_rest") then 91 if module:get_option_string("authentication") == "anonymous" and module:get_option_boolean("anonymous_rest") then
69 www_authenticate_header = nil; 92 www_authenticate_header = nil;
70 function check_credentials(request) -- luacheck: ignore 212/request 93 function check_credentials(request) -- luacheck: ignore 212/request
266 return cbor.encode(mapped); 289 return cbor.encode(mapped);
267 end 290 end
268 error "unsupported encoding"; 291 error "unsupported encoding";
269 end 292 end
270 293
271 local post_errors = errors.init("mod_rest", {
272 noauthz = { code = 401; type = "auth"; condition = "not-authorized"; text = "No credentials provided" };
273 unauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials not accepted" };
274 parse = { code = 400; type = "modify"; condition = "not-well-formed"; text = "Failed to parse payload" };
275 xmlns = { code = 422; type = "modify"; condition = "invalid-namespace"; text = "'xmlns' attribute must be empty" };
276 name = { code = 422; type = "modify"; condition = "unsupported-stanza-type"; text = "Invalid stanza, must be 'message', 'presence' or 'iq'." };
277 to = { code = 422; type = "modify"; condition = "improper-addressing"; text = "Invalid destination JID" };
278 from = { code = 422; type = "modify"; condition = "invalid-from"; text = "Invalid source JID" };
279 from_auth = { code = 403; type = "auth"; condition = "not-authorized"; text = "Not authorized to send stanza with requested 'from'" };
280 iq_type = { code = 422; type = "modify"; condition = "invalid-xml"; text = "'iq' stanza must be of type 'get' or 'set'" };
281 iq_tags = { code = 422; type = "modify"; condition = "bad-format"; text = "'iq' stanza must have exactly one child tag" };
282 mediatype = { code = 415; type = "cancel"; condition = "bad-format"; text = "Unsupported media type" };
283 size = { code = 413; type = "modify"; condition = "resource-constraint", text = "Payload too large" };
284 });
285
286 -- GET → iq-get 294 -- GET → iq-get
287 local function parse_request(request, path) 295 local function parse_request(request, path)
288 if path and request.method == "GET" then 296 if path and request.method == "GET" then
289 -- e.g. /version/{to} 297 -- e.g. /version/{to}
290 if request.url.query then 298 if request.url.query then
306 314
307 if not request.headers.authorization and www_authenticate_header then 315 if not request.headers.authorization and www_authenticate_header then
308 response.headers.www_authenticate = www_authenticate_header; 316 response.headers.www_authenticate = www_authenticate_header;
309 return post_errors.new("noauthz"); 317 return post_errors.new("noauthz");
310 else 318 else
311 origin = check_credentials(request); 319 local err;
320 origin, err = check_credentials(request);
312 if not origin then 321 if not origin then
313 return post_errors.new("unauthz"); 322 return err or post_errors.new("unauthz");
314 end 323 end
315 from = jid.join(origin.username, origin.host, origin.resource); 324 from = jid.join(origin.username, origin.host, origin.resource);
316 origin.full_jid = from; 325 origin.full_jid = from;
317 origin.type = "c2s"; 326 origin.type = "c2s";
318 origin.log = log; 327 origin.log = log;