Mercurial > prosody-modules
comparison mod_rest/mod_rest.lua @ 3910:49efd1323a1b
mod_rest: Add support for token authentication
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Wed, 26 Feb 2020 18:36:40 +0000 |
| parents | eb27e51cf2c9 |
| children | 064c32a5be7c |
comparison
equal
deleted
inserted
replaced
| 3909:eb27e51cf2c9 | 3910:49efd1323a1b |
|---|---|
| 2 -- | 2 -- |
| 3 -- Copyright (c) 2019-2020 Kim Alvefur | 3 -- Copyright (c) 2019-2020 Kim Alvefur |
| 4 -- | 4 -- |
| 5 -- This file is MIT/X11 licensed. | 5 -- This file is MIT/X11 licensed. |
| 6 | 6 |
| 7 local encodings = require "util.encodings"; | |
| 8 local base64 = encodings.base64; | |
| 7 local errors = require "util.error"; | 9 local errors = require "util.error"; |
| 8 local http = require "net.http"; | 10 local http = require "net.http"; |
| 9 local id = require "util.id"; | 11 local id = require "util.id"; |
| 10 local jid = require "util.jid"; | 12 local jid = require "util.jid"; |
| 11 local json = require "util.json"; | 13 local json = require "util.json"; |
| 12 local st = require "util.stanza"; | 14 local st = require "util.stanza"; |
| 15 local um = require "core.usermanager"; | |
| 13 local xml = require "util.xml"; | 16 local xml = require "util.xml"; |
| 14 | 17 |
| 15 local allow_any_source = module:get_host_type() == "component"; | |
| 16 local validate_from_addresses = module:get_option_boolean("validate_from_addresses", true); | |
| 17 local secret = assert(module:get_option_string("rest_credentials"), "rest_credentials is a required setting"); | |
| 18 local auth_type = assert(secret:match("^%S+"), "Format of rest_credentials MUST be like 'Bearer secret'"); | |
| 19 assert(auth_type == "Bearer" or auth_type == "Basic", "Only 'Bearer' and 'Basic' are supported in rest_credentials"); | |
| 20 | |
| 21 local jsonmap = module:require"jsonmap"; | 18 local jsonmap = module:require"jsonmap"; |
| 19 | |
| 20 local tokens = module:depends("authtokens"); | |
| 21 | |
| 22 local auth_mechanisms = module:get_option_set("rest_auth_mechanisms", { "Basic", "Bearer" }); | |
| 23 | |
| 24 local www_authenticate_header; | |
| 25 do | |
| 26 local header, realm = {}, module.host.."/"..module.name; | |
| 27 for mech in auth_mechanisms do | |
| 28 header[#header+1] = ("%s realm=%q"):format(mech, realm); | |
| 29 end | |
| 30 www_authenticate_header = table.concat(header, ", "); | |
| 31 end | |
| 32 | |
| 22 -- Bearer token | 33 -- Bearer token |
| 23 local function check_credentials(request) | 34 local function check_credentials(request) |
| 24 return request.headers.authorization == secret; | 35 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$"); |
| 25 end | 36 if not (auth_type and auth_data) or not auth_mechanisms:contains(auth_type) then |
| 26 if secret == "Basic" and module:get_host_type() == "local" then | 37 return false; |
| 27 local um = require "core.usermanager"; | 38 end |
| 28 local encodings = require "util.encodings"; | 39 |
| 29 local base64 = encodings.base64; | 40 if auth_type == "Basic" then |
| 30 | 41 local creds = base64.decode(auth_data); |
| 31 function check_credentials(request) | |
| 32 local creds = string.match(request.headers.authorization, "^Basic%s+([A-Za-z0-9+/]+=?=?)%s*$"); | |
| 33 if not creds then return false; end | |
| 34 creds = base64.decode(creds); | |
| 35 if not creds then return false; end | 42 if not creds then return false; end |
| 36 local username, password = string.match(creds, "^([^:]+):(.*)$"); | 43 local username, password = string.match(creds, "^([^:]+):(.*)$"); |
| 37 if not username then return false; end | 44 if not username then return false; end |
| 38 username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password); | 45 username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password); |
| 39 if not username then return false; end | 46 if not username then return false; end |
| 40 if not um.test_password(username, module.host, password) then | 47 if not um.test_password(username, module.host, password) then |
| 41 return false; | 48 return false; |
| 42 end | 49 end |
| 43 return jid.join(username, module.host); | 50 return { username = username, host = module.host }; |
| 44 end | 51 elseif auth_type == "Bearer" then |
| 52 local token_info = tokens.get_token_info(auth_data); | |
| 53 if not token_info or not token_info.session then | |
| 54 return false; | |
| 55 end | |
| 56 return token_info.session; | |
| 57 end | |
| 58 return nil; | |
| 45 end | 59 end |
| 46 | 60 |
| 47 local function parse(mimetype, data) | 61 local function parse(mimetype, data) |
| 48 mimetype = mimetype and mimetype:match("^[^; ]*"); | 62 mimetype = mimetype and mimetype:match("^[^; ]*"); |
| 49 if mimetype == "application/xmpp+xml" then | 63 if mimetype == "application/xmpp+xml" then |
| 82 return tostring(s); | 96 return tostring(s); |
| 83 end | 97 end |
| 84 | 98 |
| 85 local function handle_post(event) | 99 local function handle_post(event) |
| 86 local request, response = event.request, event.response; | 100 local request, response = event.request, event.response; |
| 87 local from = module.host; | 101 local from; |
| 102 local origin; | |
| 103 | |
| 88 if not request.headers.authorization then | 104 if not request.headers.authorization then |
| 89 response.headers.www_authenticate = ("%s realm=%q"):format(auth_type, module.host.."/"..module.name); | 105 response.headers.www_authenticate = www_authenticate_header; |
| 90 return 401; | 106 return 401; |
| 91 else | 107 else |
| 92 local authz = check_credentials(request); | 108 origin = check_credentials(request); |
| 93 if not authz then | 109 if not origin then |
| 94 return 401; | 110 return 401; |
| 95 end | 111 end |
| 96 if type(authz) == "string" then | 112 from = jid.join(origin.username, origin.host, origin.resource); |
| 97 from = authz; | |
| 98 end | |
| 99 end | 113 end |
| 100 local payload, err = parse(request.headers.content_type, request.body); | 114 local payload, err = parse(request.headers.content_type, request.body); |
| 101 if not payload then | 115 if not payload then |
| 102 -- parse fail | 116 -- parse fail |
| 103 return errors.new({ code = 400, text = "Failed to parse payload" }, { error = err, type = request.headers.content_type, data = request.body }); | 117 return errors.new({ code = 400, text = "Failed to parse payload" }, { error = err, type = request.headers.content_type, data = request.body }); |
| 109 end | 123 end |
| 110 local to = jid.prep(payload.attr.to); | 124 local to = jid.prep(payload.attr.to); |
| 111 if not to then | 125 if not to then |
| 112 return errors.new({ code = 422, text = "Invalid destination JID" }); | 126 return errors.new({ code = 422, text = "Invalid destination JID" }); |
| 113 end | 127 end |
| 114 if allow_any_source and payload.attr.from then | 128 if payload.attr.from then |
| 115 from = jid.prep(payload.attr.from); | 129 local requested_from = jid.prep(payload.attr.from); |
| 116 if not from then | 130 if not requested_from then |
| 117 return errors.new({ code = 422, text = "Invalid source JID" }); | 131 return errors.new({ code = 422, text = "Invalid source JID" }); |
| 118 end | 132 end |
| 119 if validate_from_addresses and not jid.compare(from, module.host) then | 133 if jid.compare(requested_from, from) then |
| 120 return errors.new({ code = 403, text = "Source JID must belong to current host" }); | 134 from = requested_from; |
| 135 else | |
| 136 return errors.new({ code = 403, text = "Not authorized to send from "..requested_from }); | |
| 121 end | 137 end |
| 122 end | 138 end |
| 123 payload.attr = { | 139 payload.attr = { |
| 124 from = from, | 140 from = from, |
| 125 to = to, | 141 to = to, |
| 128 ["xml:lang"] = payload.attr["xml:lang"], | 144 ["xml:lang"] = payload.attr["xml:lang"], |
| 129 }; | 145 }; |
| 130 module:log("debug", "Received[rest]: %s", payload:top_tag()); | 146 module:log("debug", "Received[rest]: %s", payload:top_tag()); |
| 131 local send_type = decide_type((request.headers.accept or "") ..",".. request.headers.content_type) | 147 local send_type = decide_type((request.headers.accept or "") ..",".. request.headers.content_type) |
| 132 if payload.name == "iq" then | 148 if payload.name == "iq" then |
| 149 function origin.send(stanza) | |
| 150 prosody.core_route_stanza(nil, stanza); | |
| 151 end | |
| 133 if payload.attr.type ~= "get" and payload.attr.type ~= "set" then | 152 if payload.attr.type ~= "get" and payload.attr.type ~= "set" then |
| 134 return errors.new({ code = 422, text = "'iq' stanza must be of type 'get' or 'set'" }); | 153 return errors.new({ code = 422, text = "'iq' stanza must be of type 'get' or 'set'" }); |
| 135 elseif #payload.tags ~= 1 then | 154 elseif #payload.tags ~= 1 then |
| 136 return errors.new({ code = 422, text = "'iq' stanza must have exactly one child tag" }); | 155 return errors.new({ code = 422, text = "'iq' stanza must have exactly one child tag" }); |
| 137 end | 156 end |
| 138 return module:send_iq(payload):next( | 157 return module:send_iq(payload, origin):next( |
| 139 function (result) | 158 function (result) |
| 140 module:log("debug", "Sending[rest]: %s", result.stanza:top_tag()); | 159 module:log("debug", "Sending[rest]: %s", result.stanza:top_tag()); |
| 141 response.headers.content_type = send_type; | 160 response.headers.content_type = send_type; |
| 142 return encode(send_type, result.stanza); | 161 return encode(send_type, result.stanza); |
| 143 end, | 162 end, |
| 152 else | 171 else |
| 153 return error; | 172 return error; |
| 154 end | 173 end |
| 155 end); | 174 end); |
| 156 else | 175 else |
| 157 local origin = {}; | |
| 158 function origin.send(stanza) | 176 function origin.send(stanza) |
| 159 module:log("debug", "Sending[rest]: %s", stanza:top_tag()); | 177 module:log("debug", "Sending[rest]: %s", stanza:top_tag()); |
| 160 response.headers.content_type = send_type; | 178 response.headers.content_type = send_type; |
| 161 response:send(encode(send_type, stanza)); | 179 response:send(encode(send_type, stanza)); |
| 162 return true; | 180 return true; |
