Mercurial > prosody-modules
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; |
