-
Notifications
You must be signed in to change notification settings - Fork 31
/
Copy pathmod_auth_hybrid_matrix_token.lua
410 lines (340 loc) · 11.7 KB
/
mod_auth_hybrid_matrix_token.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
-- -----------------------------------------------------------------------------
-- Hybrid Matrix-Token authentication
-- -----------------------------------------------------------------------------
-- This module is an authentication provider for Prosody which supports Matrix
-- and standard Jitsi token at the same time. It senses the type of the token
-- and handles it depending on its type.
-- -----------------------------------------------------------------------------
local async = require "util.async"
local basexx = require 'basexx'
local cjson_safe = require 'cjson.safe'
local formdecode = require "util.http".formdecode
local generate_uuid = require "util.uuid".generate
local http = require "net.http"
local json = require "util.json"
local new_sasl = require "util.sasl".new
local sasl = require "util.sasl"
local sessions = prosody.full_sessions
local token_util = module:require "token/util".new(module)
-- no token configuration
if token_util == nil then
return
end
module:depends("jitsi_session")
local uvsIssuer = {"*"}
local issuer = module:get_option("uvs_issuer", nil)
if issuer then
uvsIssuer = { string.format("%s", issuer) }
end
local uvsUrl = module:get_option("uvs_base_url", nil)
if uvsUrl == nil then
module:log("warn", "Missing 'uvs_base_url' config")
end
local uvsAuthToken = module:get_option("uvs_auth_token", nil)
if uvsAuthToken == nil then
module:log(
"info",
"No uvs_auth_token supplied, not sending authentication headers"
)
end
local measure_pre_fetch_fail = module:measure('pre_fetch_fail', 'counter')
local measure_verify_fail = module:measure('verify_fail', 'counter')
local measure_success = module:measure('success', 'counter')
local measure_ban = module:measure('ban', 'counter')
local measure_post_auth_fail = module:measure('post_auth_fail', 'counter')
-- define auth provider
local provider = {}
local host = module.host
-- Extract 'token' and 'room' params from URL when session is created
function init_session(event)
local session, request = event.session, event.request
local query = request.url.query
local token
-- extract token from Authorization header
if request.headers["authorization"] then
-- assumes the header value starts with "Bearer "
token = request.headers["authorization"]:sub(
8,
#request.headers["authorization"]
)
end
-- allow override of token via query parameter
if query ~= nil then
local params = formdecode(query)
if params then
session.jitsi_room = params.room
if params.token then
token = params.token
end
end
end
-- in either case set auth_token in the session
session.auth_token = token
session.user_agent_header = request.headers['user_agent']
end
module:hook_global("bosh-session", init_session)
module:hook_global("websocket-session", init_session)
function provider.test_password(_username, _password)
return nil, "Password based auth not supported"
end
function provider.get_password(_username)
return nil
end
function provider.set_password(_username, _password)
return nil, "Set password not supported"
end
function provider.user_exists(_username)
return nil
end
function provider.create_user(_username, _password)
return nil
end
function provider.delete_user(_username)
return nil
end
local function split_token(token)
local segments = {}
for seg in string.gmatch(token, "([^.]+)") do
table.insert(segments, seg)
end
return segments
end
local function parse_token(token)
if type(token) ~= "string" then return nil, nil, nil end
local segments = split_token(token)
if #segments ~= 3 then return nil, nil, nil end
local header, err1 = cjson_safe.decode(basexx.from_url64(segments[1]))
if err1 then return nil, nil, nil end
local payload, err2 = cjson_safe.decode(basexx.from_url64(segments[2]))
if err2 then return nil, nil, nil end
local sign, err3 = basexx.from_url64(segments[3])
if err3 then return nil, nil, nil end
return header, payload, sign
end
local function get_options(matrixPayload)
local options = {}
options.method = "POST"
options.headers = {
["Content-Type"] = "application/json",
["User-Agent"] = string.format("Prosody (%s)", prosody.version)
}
if uvsAuthToken ~= nil then
options.headers["Authorization"] = string.format(
"Bearer %s", uvsAuthToken
)
end
local body = {
["token"] = matrixPayload["token"],
["room_id"] = matrixPayload["room_id"]
}
if matrixPayload.server_name then
body["matrix_server_name"] = matrixPayload.server_name
end
options.body = json.encode(body)
return options
end
local function is_user_in_room(session, matrixPayload)
local url = string.format("%s/verify/user_in_room", uvsUrl)
local options = get_options(matrixPayload)
local wait, done = async.waiter()
local httpRes
local function cb(resBody, resCode, _req, _res)
if resCode == 200 then
httpRes = json.decode(resBody)
end
done()
end
http.request(url, options, cb)
wait()
-- no result
if not (httpRes and httpRes.results) then
return false
end
-- not a member of Matrix room
if not (httpRes.results.user and httpRes.results.room_membership) then
return false
end
-- set affiliation as session value according to their power level
session.matrix_affiliation = "member"
session.auth_matrix_user_verification_is_owner = false
if
httpRes.power_levels and httpRes.power_levels.user and
httpRes.power_levels.room and httpRes.power_levels.room.state_default and
httpRes.power_levels.user >= httpRes.power_levels.room.state_default
then
session.matrix_affiliation = "owner"
session.auth_matrix_user_verification_is_owner = true
end
return true
end
local function matrix_handler(session, payload)
if uvsUrl == nil then
module:log("warn", "Missing 'uvs_base_url' config")
session.auth_token = nil
measure_pre_fetch_fail(1)
return false, "access-denied", "missing Matrix UVS address"
end
session.public_key = "notused"
local res, error, reason = token_util:process_and_verify_token(
session,
uvsIssuer
)
if res == false then
module:log(
"warn",
"Error verifying token err:%s, reason:%s tenant:%s room:%s, agent:%s",
error,
reason,
session.jitsi_web_query_prefix,
session.jitsi_web_query_room,
session.user_agent_header
)
session.auth_token = nil
measure_verify_fail(1)
return res, error, reason
end
if not payload.context.matrix.room_id
or payload.context.matrix.room_id == ""
then
module:log("warn", "Missing Matrix room_id in token")
session.auth_token = nil
measure_verify_fail(1)
return false, "bad-request", "Matrix room ID must be given"
end
local decodedRoomId = basexx.from_base32(session.jitsi_room or "")
if decodedRoomId ~= payload.context.matrix.room_id then
module:log("warn", "Jitsi and Matrix rooms don't match")
session.auth_token = nil
measure_ban(1)
return false, "not-allowed", "Jitsi room does not match Matrix room"
end
if not is_user_in_room(session, payload.context.matrix) then
module:log("warn", "Matrix token is invalid or user does not in room")
session.auth_token = nil
measure_ban(1)
return false, "not-allowed", "Matrix token invalid or not in room"
end
session.jitsi_meet_context_matrix = payload.context.matrix
return true, nil, nil
end
local function token_handler(session)
-- retrieve custom public key from server and save it on the session
local preEvent = prosody.events.fire_event(
"pre-jitsi-authentication-fetch-key",
session
)
if preEvent ~= nil and preEvent.res == false then
module:log(
"warn",
"Error verifying token on pre authentication stage:%s, reason:%s",
preEvent.error,
preEvent.reason
)
session.auth_token = nil
measure_pre_fetch_fail(1)
return preEvent.res, preEvent.error, preEvent.reason
end
local res, error, reason = token_util:process_and_verify_token(session)
if res == false then
module:log(
"warn",
"Error verifying token err:%s, reason:%s tenant:%s room:%s, agent:%s",
error,
reason,
session.jitsi_web_query_prefix,
session.jitsi_web_query_room,
session.user_agent_header
)
session.auth_token = nil
measure_verify_fail(1)
return res, error, reason
end
return true, nil, nil
end
local function common_handler(self, session, message)
local shouldAllow = prosody.events.fire_event(
"jitsi-access-ban-check",
session
)
if shouldAllow == false then
module:log("warn", "user is banned")
measure_ban(1)
return false, "not-allowed", "user is banned"
end
local customUsername = prosody.events.fire_event(
"pre-jitsi-authentication",
session
)
if customUsername then
self.username = customUsername
elseif session.previd ~= nil then
for _, s in pairs(sessions) do
if (s.resumption_token == session.previd) then
self.username = s.username
break
end
end
else
self.username = message
end
local postEvent = prosody.events.fire_event(
"post-jitsi-authentication",
session
)
if postEvent ~= nil and postEvent.res == false then
module:log(
"warn",
"Error verifying token on post authentication :%s, reason:%s",
postEvent.error,
postEvent.reason
)
session.auth_token = nil
measure_post_auth_fail(1)
return postEvent.res, postEvent.error, postEvent.reason
end
measure_success(1)
return true, nil, nil
end
function provider.get_sasl_handler(session)
local function handler(self, message)
local payload
if session.auth_token then
_, payload, _ = parse_token(session.auth_token)
end
if payload and payload.context and payload.context.matrix then
module:log("info", "Matrix authentication handler is selected")
local res, error, reason = matrix_handler(session, payload)
if res == false then
return res, error, reason
end
else
module:log("info", "Token authentication handler is selected")
local res, error, reason = token_handler(session)
if res == false then
return res, error, reason
end
end
local res, error, reason = common_handler(self, session, message)
if res == false then
return res, error, reason
end
return true
end
return new_sasl(host, { anonymous = handler })
end
module:provides("auth", provider)
local function anonymous(self, message)
module:log("debug", "Message in anonymous: %s", message)
local username = generate_uuid()
-- This calls the handler created in 'provider.get_sasl_handler(session)'
local result, err, msg = self.profile.anonymous(self, username, self.realm)
if result == true then
if (self.username == nil) then
self.username = username
end
return "success"
else
return "failure", err, msg
end
end
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous)