Comparison

plugins/mod_tokenauth.lua @ 12998:601d9a375b86

mod_tokenauth: Refactor API to separate tokens and grants This is another iteration on top of the previous sub-tokens work. Essentially, the concept of a "parent token" has been replaced with the concept of a "grant" to which all tokens now belong. The grant does not have any tokens when first created, but the create_token() call can add them.
author Matthew Wild <mwild1@gmail.com>
date Mon, 27 Mar 2023 18:35:57 +0100
parent 12997:0a56b84ec4ad
child 12999:c87ac7d1967f
comparison
equal deleted inserted replaced
12997:0a56b84ec4ad 12998:601d9a375b86
4 local jid = require "prosody.util.jid"; 4 local jid = require "prosody.util.jid";
5 local random = require "prosody.util.random"; 5 local random = require "prosody.util.random";
6 local usermanager = require "prosody.core.usermanager"; 6 local usermanager = require "prosody.core.usermanager";
7 local generate_identifier = require "prosody.util.id".short; 7 local generate_identifier = require "prosody.util.id".short;
8 8
9 local token_store = module:open_store("auth_tokens", "map"); 9 local token_store = module:open_store("auth_tokens", "keyval+");
10 10
11 local access_time_granularity = module:get_option_number("token_auth_access_time_granularity", 60); 11 local access_time_granularity = module:get_option_number("token_auth_access_time_granularity", 60);
12 12
13 local function select_role(username, host, role) 13 local function select_role(username, host, role)
14 if role then 14 if role then
15 return prosody.hosts[host].authz.get_role_by_name(role); 15 return prosody.hosts[host].authz.get_role_by_name(role);
16 end 16 end
17 return usermanager.get_user_role(username, host); 17 return usermanager.get_user_role(username, host);
18 end 18 end
19 19
20 function create_jid_token(actor_jid, token_jid, token_role, token_ttl, token_data, token_purpose) 20 function create_grant(actor_jid, grant_jid, grant_ttl, grant_data)
21 token_jid = jid.prep(token_jid); 21 grant_jid = jid.prep(grant_jid);
22 if not actor_jid or token_jid ~= actor_jid and not jid.compare(token_jid, actor_jid) then 22 if not actor_jid or actor_jid ~= grant_jid and not jid.compare(grant_jid, actor_jid) then
23 return nil, "not-authorized"; 23 module:log("debug", "Actor <%s> is not permitted to create a token granting access to JID <%s>", actor_jid, grant_jid);
24 end 24 return nil, "not-authorized";
25 25 end
26 local token_username, token_host, token_resource = jid.split(token_jid); 26
27 27 local grant_username, grant_host, grant_resource = jid.split(grant_jid);
28 if token_host ~= module.host then 28
29 return nil, "invalid-host"; 29 if grant_host ~= module.host then
30 end 30 return nil, "invalid-host";
31 31 end
32
33 local grant_id = id.short();
34 local now = os.time();
35
36 local grant = {
37 id = grant_id;
38
39 owner = actor_jid;
40 created = now;
41 expires = grant_ttl and (now + grant_ttl) or nil;
42 accessed = now;
43
44 jid = grant_jid;
45 resource = grant_resource;
46
47 data = grant_data;
48
49 -- tokens[<hash-name>..":"..<secret>] = token_info
50 tokens = {};
51 };
52
53 local ok, err = token_store:set_key(grant_username, grant_id, grant);
54 if not ok then
55 return nil, err;
56 end
57
58 return grant;
59 end
60
61 function create_token(grant_jid, grant, token_role, token_ttl, token_purpose, token_data)
32 if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then 62 if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then
33 return nil, "bad-request"; 63 return nil, "bad-request";
34 end 64 end
35 65 local grant_username, grant_host = jid.split(grant_jid);
36 local token_id = id.short(); 66 if grant_host ~= module.host then
67 return nil, "invalid-host";
68 end
69 if type(grant) == "string" then -- lookup by id
70 grant = token_store:get_key(grant_username, grant);
71 if not grant then return nil; end
72 end
73
74 if not grant.tokens then return nil, "internal-server-error"; end -- old-style token?
37 75
38 local now = os.time(); 76 local now = os.time();
77 local expires = grant.expires; -- Default to same expiry as grant
78 if token_ttl then -- explicit lifetime requested
79 if expires then
80 -- Grant has an expiry, so limit to that or shorter
81 expires = math.min(now + token_ttl, expires);
82 else
83 -- Grant never expires, just use whatever expiry is requested for the token
84 expires = now + token_ttl;
85 end
86 end
39 87
40 local token_info = { 88 local token_info = {
41 id = token_id; 89 role = token_role;
42 90
43 owner = actor_jid;
44 created = now; 91 created = now;
45 expires = token_ttl and (now + token_ttl) or nil; 92 expires = expires;
46 accessed = now;
47 jid = token_jid;
48 purpose = token_purpose; 93 purpose = token_purpose;
49 94
50 resource = token_resource;
51 role = token_role;
52 data = token_data; 95 data = token_data;
53 }; 96 };
54 97
55 local token_secret = random.bytes(18); 98 local token_secret = random.bytes(18);
56 local token = "secret-token:"..base64.encode("2;"..token_id..";"..token_secret..";"..jid.join(token_username, token_host)); 99 grant.tokens["sha256:"..hashes.sha256(token_secret, true)] = token_info;
57 local ok, err = token_store:set(token_username, token_id, { 100
58 secret_sha256 = hashes.sha256(token_secret, true); 101 local ok, err = token_store:set_key(grant_username, grant.id, grant);
59 token_info = token_info
60 });
61 if not ok then 102 if not ok then
62 return nil, err; 103 return nil, err;
63 end 104 end
64 105
65 return token, token_info; 106 local token_string = "secret-token:"..base64.encode("2;"..grant.id..";"..token_secret..";"..grant.jid);
66 end 107 return token_string, token_info;
67
68 function create_sub_token(actor_jid, parent_id, token_role, token_ttl, token_data, token_purpose)
69 local username, host = jid.split(actor_jid);
70 if host ~= module.host then
71 return nil, "invalid-host";
72 end
73
74 if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then
75 return nil, "bad-request";
76 end
77
78 -- Find parent token
79 local parent_token = token_store:get(username, parent_id);
80 if not parent_token then return nil; end
81 local token_info = parent_token.token_info;
82
83 local now = os.time();
84 local expires = token_info.expires; -- Default to same expiry as parent token
85 if token_ttl then
86 if expires then
87 -- Parent token has an expiry, so limit to that or shorter
88 expires = math.min(now + token_ttl, expires);
89 else
90 -- Parent token never expires, just add whatever expiry is requested
91 expires = now + token_ttl;
92 end
93 end
94
95 local sub_token_info = {
96 id = parent_id;
97 type = "subtoken";
98 role = token_role or token_info.role;
99 jid = token_info.jid;
100 created = now;
101 expires = expires;
102 purpose = token_purpose or token_info.purpose;
103 data = token_data;
104 };
105
106 local sub_tokens = parent_token.sub_tokens;
107 if not sub_tokens then
108 sub_tokens = {};
109 parent_token.sub_tokens = sub_tokens;
110 end
111
112 local sub_token_secret = random.bytes(18);
113 sub_tokens[hashes.sha256(sub_token_secret, true)] = sub_token_info;
114
115 local sub_token = "secret-token:"..base64.encode("2;"..token_info.id..";"..sub_token_secret..";"..token_info.jid);
116
117 local ok, err = token_store:set(username, parent_id, parent_token);
118 if not ok then
119 return nil, err;
120 end
121
122 return sub_token, sub_token_info;
123 end
124
125 local function clear_expired_sub_tokens(username, token_id)
126 local sub_tokens = token_store:get_key(username, token_id, "sub_tokens");
127 if not sub_tokens then return; end
128 local now = os.time();
129 for secret, info in pairs(sub_tokens) do
130 if info.expires < now then
131 sub_tokens[secret] = nil;
132 end
133 end
134 end 108 end
135 109
136 local function parse_token(encoded_token) 110 local function parse_token(encoded_token)
137 if not encoded_token then return nil; end 111 if not encoded_token then return nil; end
138 local encoded_data = encoded_token:match("^secret%-token:(.+)$"); 112 local encoded_data = encoded_token:match("^secret%-token:(.+)$");
143 if not token_id then return nil; end 117 if not token_id then return nil; end
144 local token_user, token_host = jid.split(token_jid); 118 local token_user, token_host = jid.split(token_jid);
145 return token_id, token_user, token_host, token_secret; 119 return token_id, token_user, token_host, token_secret;
146 end 120 end
147 121
148 local function _validate_token_info(token_user, token_id, token_info, sub_token_info) 122 local function clear_expired_grant_tokens(grant, now)
149 local now = os.time(); 123 local updated;
150 if token_info.expires and token_info.expires < now then 124 now = now or os.time();
151 if token_info.type == "subtoken" then 125 for secret, token_info in pairs(grant.tokens) do
152 clear_expired_sub_tokens(token_user, token_id); 126 if token_info.expires < now then
153 else 127 grant.tokens[secret] = nil;
154 token_store:set(token_user, token_id, nil); 128 updated = true;
155 end 129 end
156 return nil, "not-authorized"; 130 end
157 end 131 return updated;
158
159 if token_info.type ~= "subtoken" then
160 local account_info = usermanager.get_account_info(token_user, module.host);
161 local password_updated_at = account_info and account_info.password_updated;
162 if password_updated_at and password_updated_at > token_info.created then
163 token_store:set(token_user, token_id, nil);
164 return nil, "not-authorized";
165 end
166
167 -- Update last access time if necessary
168 local last_accessed = token_info.accessed;
169 if not last_accessed or (now - last_accessed) > access_time_granularity then
170 token_info.accessed = now;
171 token_store:set_key(token_user, token_id, "token_info", token_info);
172 end
173 end
174
175 if sub_token_info then
176 -- Parent token validated, now validate (and return) the subtoken
177 return _validate_token_info(token_user, token_id, sub_token_info);
178 end
179
180 return token_info
181 end 132 end
182 133
183 local function _get_validated_token_info(token_id, token_user, token_host, token_secret) 134 local function _get_validated_token_info(token_id, token_user, token_host, token_secret)
184 if token_host ~= module.host then 135 if token_host ~= module.host then
185 return nil, "invalid-host"; 136 return nil, "invalid-host";
186 end 137 end
187 138
188 local token, err = token_store:get(token_user, token_id); 139 local grant, err = token_store:get_key(token_user, token_id);
189 if not token then 140 if not grant or not grant.tokens then
190 if err then 141 if err then
142 module:log("error", "Unable to read from token storage: %s", err);
191 return nil, "internal-error"; 143 return nil, "internal-error";
192 end 144 end
193 return nil, "not-authorized"; 145 module:log("warn", "Invalid token in storage (%s / %s)", token_user, token_id);
194 elseif not token.secret_sha256 then -- older token format
195 token_store:set(token_user, token_id, nil);
196 return nil, "not-authorized"; 146 return nil, "not-authorized";
197 end 147 end
198 148
199 -- Check provided secret 149 -- Check provided secret
200 local secret_hash = hashes.sha256(token_secret, true); 150 local secret_hash = "sha256:"..hashes.sha256(token_secret, true);
201 if not hashes.equals(secret_hash, token.secret_sha256) then 151 local token_info = grant.tokens[secret_hash];
202 local sub_token_info = token.sub_tokens and token.sub_tokens[secret_hash]; 152 if not token_info then
203 if sub_token_info then 153 module:log("debug", "No tokens matched the given secret");
204 return _validate_token_info(token_user, token_id, token.token_info, sub_token_info); 154 return nil, "not-authorized";
205 end 155 end
206 return nil, "not-authorized"; 156
207 end 157 -- Check expiry
208 158 local now = os.time();
209 return _validate_token_info(token_user, token_id, token.token_info); 159 if token_info.expires < now then
210 160 module:log("debug", "Token has expired, cleaning it up");
211 end 161 grant.tokens[secret_hash] = nil;
162 token_store:set_key(token_user, token_id, grant);
163 return nil, "not-authorized";
164 end
165
166 -- Invalidate grants from before last password change
167 local account_info = usermanager.get_account_info(token_user, module.host);
168 local password_updated_at = account_info and account_info.password_updated;
169 if grant.created < password_updated_at and password_updated_at then
170 module:log("debug", "Token grant issued before last password change, invalidating it now");
171 token_store:set_key(token_user, token_id, nil);
172 return nil, "not-authorized";
173 end
174
175 -- Update last access time if necessary
176 local last_accessed = grant.accessed;
177 if not last_accessed or (now - last_accessed) > access_time_granularity then
178 grant.accessed = now;
179 clear_expired_grant_tokens(grant); -- Clear expired tokens while we're here
180 token_store:set_key(token_user, token_id, grant);
181 end
182
183 token_info.id = token_id;
184 token_info.grant = grant;
185 token_info.jid = grant.jid;
186
187 return token_info;
188 end
189
212 190
213 function get_token_info(token) 191 function get_token_info(token)
214 local token_id, token_user, token_host, token_secret = parse_token(token); 192 local token_id, token_user, token_host, token_secret = parse_token(token);
215 if not token_id then 193 if not token_id then
216 module:log("warn", "Failed to verify access token: %s", token_user); 194 module:log("warn", "Failed to verify access token: %s", token_user);
235 resource = token_info.resource or resource or generate_identifier(); 213 resource = token_info.resource or resource or generate_identifier();
236 214
237 role = select_role(token_user, token_host, token_info.role); 215 role = select_role(token_user, token_host, token_info.role);
238 }; 216 };
239 end 217 end
240
241 218
242 function revoke_token(token) 219 function revoke_token(token)
243 local token_id, token_user, token_host = parse_token(token); 220 local token_id, token_user, token_host = parse_token(token);
244 if not token_id then 221 if not token_id then
245 module:log("warn", "Failed to verify access token: %s", token_user); 222 module:log("warn", "Failed to verify access token: %s", token_user);
256 local token_info, err = get_token_info(token); 233 local token_info, err = get_token_info(token);
257 if not token_info then 234 if not token_info then
258 module:log("debug", "SASL handler failed to verify token: %s", err); 235 module:log("debug", "SASL handler failed to verify token: %s", err);
259 return nil, nil, extra; 236 return nil, nil, extra;
260 end 237 end
261 local token_user, token_host, resource = jid.split(token_info.jid); 238 local token_user, token_host, resource = jid.split(token_info.grant.jid);
262 if realm ~= token_host or (purpose and token_info.purpose ~= purpose) then 239 if realm ~= token_host or (purpose and token_info.purpose ~= purpose) then
263 return nil, nil, extra; 240 return nil, nil, extra;
264 end 241 end
265 if auth_provider.is_enabled and not auth_provider.is_enabled(token_user) then 242 if auth_provider.is_enabled and not auth_provider.is_enabled(token_user) then
266 return true, false, token_info; 243 return true, false, token_info;