Browse Source

Merge pull request #808 from SamWhited/auth_token_asap

Add support for fetching public keys from a server to mod_auth_token
master
Paweł Domas 8 years ago
parent
commit
84834dc4e6
2 changed files with 92 additions and 36 deletions
  1. 88
    16
      prosody-plugins/mod_auth_token.lua
  2. 4
    20
      prosody-plugins/token/util.lib.lua

+ 88
- 16
prosody-plugins/mod_auth_token.lua View File

@@ -1,10 +1,16 @@
1 1
 -- Token authentication
2 2
 -- Copyright (C) 2015 Atlassian
3 3
 
4
+local basexx = require 'basexx';
5
+local have_async, async = pcall(require, "util.async");
6
+local formdecode = require "util.http".formdecode;
4 7
 local generate_uuid = require "util.uuid".generate;
8
+local http = require "net.http";
9
+local json = require 'cjson'
10
+json.encode_empty_table('array')
5 11
 local new_sasl = require "util.sasl".new;
6 12
 local sasl = require "util.sasl";
7
-local formdecode = require "util.http".formdecode;
13
+local timer = require "util.timer";
8 14
 local token_util = module:require "token/util";
9 15
 
10 16
 -- define auth provider
@@ -14,9 +20,14 @@ local host = module.host;
14 20
 
15 21
 local appId = module:get_option_string("app_id");
16 22
 local appSecret = module:get_option_string("app_secret");
23
+local asapKeyServer = module:get_option_string("asap_key_server");
17 24
 local allowEmptyToken = module:get_option_boolean("allow_empty_token");
18 25
 local disableRoomNameConstraints = module:get_option_boolean("disable_room_name_constraints");
19 26
 
27
+-- TODO: Figure out a less arbitrary default cache size.
28
+local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128);
29
+local cache = require"util.cache".new(cacheSize);
30
+
20 31
 if allowEmptyToken == true then
21 32
 	module:log("warn", "WARNING - empty tokens allowed");
22 33
 end
@@ -26,8 +37,13 @@ if appId == nil then
26 37
 	return;
27 38
 end
28 39
 
29
-if appSecret == nil then
30
-	module:log("error", "'app_secret' must not be empty");
40
+if appSecret == nil and asapKeyServer == nil then
41
+	module:log("error", "'app_secret' or 'asap_key_server' must be specified");
42
+	return;
43
+end
44
+
45
+if asapKeyServer and not have_async then
46
+	module:log("error", "requires a version of Prosody with util.async");
31 47
 	return;
32 48
 end
33 49
 
@@ -64,6 +80,52 @@ function provider.delete_user(username)
64 80
 	return nil;
65 81
 end
66 82
 
83
+local http_timeout = 30;
84
+local http_headers = {
85
+	["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")"
86
+};
87
+
88
+function get_public_key(keyId)
89
+	local content = cache:get(keyId);
90
+	if content == nil then
91
+		-- If the key is not found in the cache.
92
+		module:log("debug", "Cache miss for key: "..keyId);
93
+		local code;
94
+		local wait, done = async.waiter();
95
+		local function cb(content_, code_, response_, request_)
96
+			content, code = content_, code_;
97
+			done();
98
+		end
99
+		module:log("debug", "Fetching public key from: "..asapKeyServer..keyId);
100
+		local request = http.request(asapKeyServer..keyId, {
101
+			headers = http_headers or {},
102
+			method = "GET"
103
+		}, cb);
104
+		-- TODO: Is the done() call racey? Can we cancel this if the request
105
+		--       succeedes?
106
+		local function cancel()
107
+			-- TODO: This check is racey. Not likely to be a problem, but we should
108
+			--       still stick a mutex on content / code at some point.
109
+			if code == nil then
110
+				http.destroy_request(request);
111
+				done();
112
+			end
113
+		end
114
+		timer.add_task(http_timeout, cancel);
115
+		wait();
116
+
117
+		if code == 200 or code == 204 then
118
+			return content;
119
+		end
120
+	else
121
+		-- If the key is in the cache, use it.
122
+		module:log("debug", "Cache hit for key: "..keyId);
123
+		return content;
124
+	end
125
+
126
+	return nil;
127
+end
128
+
67 129
 function provider.get_sasl_handler(session)
68 130
 	-- JWT token extracted from BOSH URL
69 131
 	local token = session.auth_token;
@@ -71,29 +133,39 @@ function provider.get_sasl_handler(session)
71 133
 	local function get_username_from_token(self, message)
72 134
 
73 135
 		if token == nil then
74
-			if allowEmptyToken == true then
136
+			if allowEmptyToken then
75 137
 				return true;
76 138
 			else
77 139
 				return false, "not-allowed", "token required";
78 140
 			end
79 141
 		end
80 142
 
81
-		-- here we check if 'room' claim exists
82
-		local room, roomErr = token_util.get_room_name(token, appSecret);
83
-		if room == nil and disableRoomNameConstraints ~= true then
84
-            if roomErr == nil then
85
-                roomErr = "'room' claim is missing";
86
-            end
87
-			return false, "not-allowed", roomErr;
143
+		local pubKey;
144
+		if asapKeyServer and session.auth_token ~= nil then
145
+			local dotFirst = session.auth_token:find("%.")
146
+			if not dotFirst then return nil, "Invalid token" end
147
+			local header = json.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1)))
148
+			local kid = header["kid"]
149
+			if kid == nil then
150
+				return false, "not-allowed", "'kid' claim is missing";
151
+			end
152
+			pubKey = get_public_key(kid);
153
+			if pubKey == nil then
154
+				return false, "not-allowed", "could not obtain public key";
155
+			end
88 156
 		end
89 157
 
90 158
 		-- now verify the whole token
91
-		local result, msg
92
-		= token_util.verify_token(token, appId, appSecret, room, disableRoomNameConstraints);
93
-		if result == true then
159
+		local claims, msg;
160
+		if asapKeyServer then
161
+			claims, msg = token_util.verify_token(token, appId, pubKey, disableRoomNameConstraints);
162
+		else
163
+			claims, msg = token_util.verify_token(token, appId, appSecret, disableRoomNameConstraints);
164
+		end
165
+		if claims ~= nil then
94 166
 			-- Binds room name to the session which is later checked on MUC join
95
-			session.jitsi_meet_room = room;
96
-			return true
167
+			session.jitsi_meet_room = claims["room"];
168
+			return true;
97 169
 		else
98 170
 			return false, "not-allowed", msg
99 171
 		end

+ 4
- 20
prosody-plugins/token/util.lib.lua View File

@@ -5,16 +5,7 @@ local jwt = require "luajwtjitsi";
5 5
 
6 6
 local _M = {};
7 7
 
8
-local function _get_room_name(token, appSecret)
9
-	local claims, err = jwt.decode(token, appSecret);
10
-	if claims ~= nil then
11
-		return claims["room"];
12
-	else
13
-		return nil, err;
14
-	end
15
-end
16
-
17
-local function _verify_token(token, appId, appSecret, roomName, disableRoomNameConstraints)
8
+local function _verify_token(token, appId, appSecret, disableRoomNameConstraints)
18 9
 
19 10
 	local claims, err = jwt.decode(token, appSecret, true);
20 11
 	if claims == nil then
@@ -38,19 +29,12 @@ local function _verify_token(token, appId, appSecret, roomName, disableRoomNameC
38 29
 	if roomClaim == nil and disableRoomNameConstraints ~= true then
39 30
 		return nil, "'room' claim is missing";
40 31
 	end
41
-	if roomName ~= nil and roomName ~= roomClaim and disableRoomNameConstraints ~= true then
42
-		return nil, "Invalid room name('room' claim)";
43
-	end
44
-
45
-	return true;
46
-end
47 32
 
48
-function _M.verify_token(token, appId, appSecret, roomName, disableRoomNameConstraints)
49
-	return _verify_token(token, appId, appSecret, roomName, disableRoomNameConstraints);
33
+	return claims;
50 34
 end
51 35
 
52
-function _M.get_room_name(token, appSecret)
53
-	return _get_room_name(token, appSecret);
36
+function _M.verify_token(token, appId, appSecret, disableRoomNameConstraints)
37
+	return _verify_token(token, appId, appSecret, disableRoomNameConstraints);
54 38
 end
55 39
 
56 40
 return _M;

Loading…
Cancel
Save