Changeset

12997:0a56b84ec4ad

mod_tokenauth: Support for creating sub-tokens Properties of sub-tokens: - They share the same id as their parent token - Sub-tokens may not have their own sub-tokens (but may have sibling tokens) - They always have the same or shorter lifetime compared to their parent token - Revoking a parent token revokes all sub-tokens - Sub-tokens always have the same JID as the parent token - They do not have their own 'accessed' property - accessing a sub-token updates the parent token's accessed time Although this is a generic API, it is designed to at least fill the needs of OAuth2 refresh + access tokens (where the parent token is the refresh token and the sub-tokens are access tokens).
author Matthew Wild <mwild1@gmail.com>
date Sun, 26 Mar 2023 16:46:48 +0100
parents 12996:e8716515405e
children 12998:601d9a375b86
files plugins/mod_tokenauth.lua
diffstat 1 files changed, 110 insertions(+), 22 deletions(-) [+]
line wrap: on
line diff
--- a/plugins/mod_tokenauth.lua	Sun Mar 26 15:53:27 2023 +0100
+++ b/plugins/mod_tokenauth.lua	Sun Mar 26 16:46:48 2023 +0100
@@ -65,6 +65,74 @@
 	return token, token_info;
 end
 
+function create_sub_token(actor_jid, parent_id, token_role, token_ttl, token_data, token_purpose)
+	local username, host = jid.split(actor_jid);
+	if host ~= module.host then
+		return nil, "invalid-host";
+	end
+
+	if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then
+		return nil, "bad-request";
+	end
+
+	-- Find parent token
+	local parent_token = token_store:get(username, parent_id);
+	if not parent_token then return nil; end
+	local token_info = parent_token.token_info;
+
+	local now = os.time();
+	local expires = token_info.expires; -- Default to same expiry as parent token
+	if token_ttl then
+		if expires then
+			-- Parent token has an expiry, so limit to that or shorter
+			expires = math.min(now + token_ttl, expires);
+		else
+			-- Parent token never expires, just add whatever expiry is requested
+			expires = now + token_ttl;
+		end
+	end
+
+	local sub_token_info = {
+		id = parent_id;
+		type = "subtoken";
+		role = token_role or token_info.role;
+		jid = token_info.jid;
+		created = now;
+		expires = expires;
+		purpose = token_purpose or token_info.purpose;
+		data = token_data;
+	};
+
+	local sub_tokens = parent_token.sub_tokens;
+	if not sub_tokens then
+		sub_tokens = {};
+		parent_token.sub_tokens = sub_tokens;
+	end
+
+	local sub_token_secret = random.bytes(18);
+	sub_tokens[hashes.sha256(sub_token_secret, true)] = sub_token_info;
+
+	local sub_token = "secret-token:"..base64.encode("2;"..token_info.id..";"..sub_token_secret..";"..token_info.jid);
+
+	local ok, err = token_store:set(username, parent_id, parent_token);
+	if not ok then
+		return nil, err;
+	end
+
+	return sub_token, sub_token_info;
+end
+
+local function clear_expired_sub_tokens(username, token_id)
+	local sub_tokens = token_store:get_key(username, token_id, "sub_tokens");
+	if not sub_tokens then return; end
+	local now = os.time();
+	for secret, info in pairs(sub_tokens) do
+		if info.expires < now then
+			sub_tokens[secret] = nil;
+		end
+	end
+end
+
 local function parse_token(encoded_token)
 	if not encoded_token then return nil; end
 	local encoded_data = encoded_token:match("^secret%-token:(.+)$");
@@ -77,6 +145,41 @@
 	return token_id, token_user, token_host, token_secret;
 end
 
+local function _validate_token_info(token_user, token_id, token_info, sub_token_info)
+	local now = os.time();
+	if token_info.expires and token_info.expires < now then
+		if token_info.type == "subtoken" then
+			clear_expired_sub_tokens(token_user, token_id);
+		else
+			token_store:set(token_user, token_id, nil);
+		end
+		return nil, "not-authorized";
+	end
+
+	if token_info.type ~= "subtoken" then
+		local account_info = usermanager.get_account_info(token_user, module.host);
+		local password_updated_at = account_info and account_info.password_updated;
+		if password_updated_at and password_updated_at > token_info.created then
+			token_store:set(token_user, token_id, nil);
+			return nil, "not-authorized";
+		end
+
+		-- Update last access time if necessary
+		local last_accessed = token_info.accessed;
+		if not last_accessed or (now - last_accessed) > access_time_granularity then
+			token_info.accessed = now;
+			token_store:set_key(token_user, token_id, "token_info", token_info);
+		end
+	end
+
+	if sub_token_info then
+		-- Parent token validated, now validate (and return) the subtoken
+		return _validate_token_info(token_user, token_id, sub_token_info);
+	end
+
+	return token_info
+end
+
 local function _get_validated_token_info(token_id, token_user, token_host, token_secret)
 	if token_host ~= module.host then
 		return nil, "invalid-host";
@@ -94,32 +197,17 @@
 	end
 
 	-- Check provided secret
-	if not hashes.equals(hashes.sha256(token_secret, true), token.secret_sha256) then
-		return nil, "not-authorized";
-	end
-
-	local token_info = token.token_info;
-
-	local now = os.time();
-	if token_info.expires and token_info.expires < now then
-		token_store:set(token_user, token_id, nil);
+	local secret_hash = hashes.sha256(token_secret, true);
+	if not hashes.equals(secret_hash, token.secret_sha256) then
+		local sub_token_info = token.sub_tokens and token.sub_tokens[secret_hash];
+		if sub_token_info then
+			return _validate_token_info(token_user, token_id, token.token_info, sub_token_info);
+		end
 		return nil, "not-authorized";
 	end
 
-	local account_info = usermanager.get_account_info(token_user, module.host);
-	local password_updated_at = account_info and account_info.password_updated;
-	if password_updated_at and password_updated_at > token_info.created then
-		token_store:set(token_user, token_id, nil);
-		return nil, "not-authorized";
-	end
+	return _validate_token_info(token_user, token_id, token.token_info);
 
-	local last_accessed = token_info.accessed;
-	if not last_accessed or (now - last_accessed) > access_time_granularity then
-		token_info.accessed = now;
-		token_store:set(token_user, token_id, token_info);
-	end
-
-	return token_info
 end
 
 function get_token_info(token)