Changeset

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
parents 12997:0a56b84ec4ad
children 12999:c87ac7d1967f
files plugins/mod_tokenauth.lua
diffstat 1 files changed, 92 insertions(+), 115 deletions(-) [+]
line wrap: on
line diff
--- a/plugins/mod_tokenauth.lua	Sun Mar 26 16:46:48 2023 +0100
+++ b/plugins/mod_tokenauth.lua	Mon Mar 27 18:35:57 2023 +0100
@@ -6,7 +6,7 @@
 local usermanager = require "prosody.core.usermanager";
 local generate_identifier = require "prosody.util.id".short;
 
-local token_store = module:open_store("auth_tokens", "map");
+local token_store = module:open_store("auth_tokens", "keyval+");
 
 local access_time_granularity = module:get_option_number("token_auth_access_time_granularity", 60);
 
@@ -17,120 +17,94 @@
 	return usermanager.get_user_role(username, host);
 end
 
-function create_jid_token(actor_jid, token_jid, token_role, token_ttl, token_data, token_purpose)
-	token_jid = jid.prep(token_jid);
-	if not actor_jid or token_jid ~= actor_jid and not jid.compare(token_jid, actor_jid) then
+function create_grant(actor_jid, grant_jid, grant_ttl, grant_data)
+	grant_jid = jid.prep(grant_jid);
+	if not actor_jid or actor_jid ~= grant_jid and not jid.compare(grant_jid, actor_jid) then
+		module:log("debug", "Actor <%s> is not permitted to create a token granting access to JID <%s>", actor_jid, grant_jid);
 		return nil, "not-authorized";
 	end
 
-	local token_username, token_host, token_resource = jid.split(token_jid);
+	local grant_username, grant_host, grant_resource = jid.split(grant_jid);
 
-	if token_host ~= module.host then
+	if grant_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
-
-	local token_id = id.short();
-
+	local grant_id = id.short();
 	local now = os.time();
 
-	local token_info = {
-		id = token_id;
+	local grant = {
+		id = grant_id;
 
 		owner = actor_jid;
 		created = now;
-		expires = token_ttl and (now + token_ttl) or nil;
+		expires = grant_ttl and (now + grant_ttl) or nil;
 		accessed = now;
-		jid = token_jid;
-		purpose = token_purpose;
+
+		jid = grant_jid;
+		resource = grant_resource;
 
-		resource = token_resource;
-		role = token_role;
-		data = token_data;
+		data = grant_data;
+
+		-- tokens[<hash-name>..":"..<secret>] = token_info
+		tokens = {};
 	};
 
-	local token_secret = random.bytes(18);
-	local token = "secret-token:"..base64.encode("2;"..token_id..";"..token_secret..";"..jid.join(token_username, token_host));
-	local ok, err = token_store:set(token_username, token_id, {
-		secret_sha256 = hashes.sha256(token_secret, true);
-		token_info = token_info
-	});
+	local ok, err = token_store:set_key(grant_username, grant_id, grant);
 	if not ok then
 		return nil, err;
 	end
 
-	return token, token_info;
+	return grant;
 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
-
+function create_token(grant_jid, grant, token_role, token_ttl, token_purpose, token_data)
 	if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then
 		return nil, "bad-request";
 	end
+	local grant_username, grant_host = jid.split(grant_jid);
+	if grant_host ~= module.host then
+		return nil, "invalid-host";
+	end
+	if type(grant) == "string" then -- lookup by id
+		grant = token_store:get_key(grant_username, grant);
+		if not grant then return nil; end
+	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;
+	if not grant.tokens then return nil, "internal-server-error"; end -- old-style token?
 
 	local now = os.time();
-	local expires = token_info.expires; -- Default to same expiry as parent token
-	if token_ttl then
+	local expires = grant.expires; -- Default to same expiry as grant
+	if token_ttl then -- explicit lifetime requested
 		if expires then
-			-- Parent token has an expiry, so limit to that or shorter
+			-- Grant 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
+			-- Grant never expires, just use whatever expiry is requested for the token
 			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;
+	local token_info = {
+		role = token_role;
+
 		created = now;
 		expires = expires;
-		purpose = token_purpose or token_info.purpose;
+		purpose = token_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 token_secret = random.bytes(18);
+	grant.tokens["sha256:"..hashes.sha256(token_secret, true)] = token_info;
 
-	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);
+	local ok, err = token_store:set_key(grant_username, grant.id, grant);
 	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
+	local token_string = "secret-token:"..base64.encode("2;"..grant.id..";"..token_secret..";"..grant.jid);
+	return token_string, token_info;
 end
 
 local function parse_token(encoded_token)
@@ -145,39 +119,16 @@
 	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);
+local function clear_expired_grant_tokens(grant, now)
+	local updated;
+	now = now or os.time();
+	for secret, token_info in pairs(grant.tokens) do
+		if token_info.expires < now then
+			grant.tokens[secret] = nil;
+			updated = true;
 		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
+	return updated;
 end
 
 local function _get_validated_token_info(token_id, token_user, token_host, token_secret)
@@ -185,31 +136,58 @@
 		return nil, "invalid-host";
 	end
 
-	local token, err = token_store:get(token_user, token_id);
-	if not token then
+	local grant, err = token_store:get_key(token_user, token_id);
+	if not grant or not grant.tokens then
 		if err then
+			module:log("error", "Unable to read from token storage: %s", err);
 			return nil, "internal-error";
 		end
-		return nil, "not-authorized";
-	elseif not token.secret_sha256 then -- older token format
-		token_store:set(token_user, token_id, nil);
+		module:log("warn", "Invalid token in storage (%s / %s)", token_user, token_id);
 		return nil, "not-authorized";
 	end
 
 	-- Check provided secret
-	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
+	local secret_hash = "sha256:"..hashes.sha256(token_secret, true);
+	local token_info = grant.tokens[secret_hash];
+	if not token_info then
+		module:log("debug", "No tokens matched the given secret");
+		return nil, "not-authorized";
+	end
+
+	-- Check expiry
+	local now = os.time();
+	if token_info.expires < now then
+		module:log("debug", "Token has expired, cleaning it up");
+		grant.tokens[secret_hash] = nil;
+		token_store:set_key(token_user, token_id, grant);
 		return nil, "not-authorized";
 	end
 
-	return _validate_token_info(token_user, token_id, token.token_info);
+	-- Invalidate grants from before last password change
+	local account_info = usermanager.get_account_info(token_user, module.host);
+	local password_updated_at = account_info and account_info.password_updated;
+	if grant.created < password_updated_at and password_updated_at then
+		module:log("debug", "Token grant issued before last password change, invalidating it now");
+		token_store:set_key(token_user, token_id, nil);
+		return nil, "not-authorized";
+	end
 
+	-- Update last access time if necessary
+	local last_accessed = grant.accessed;
+	if not last_accessed or (now - last_accessed) > access_time_granularity then
+		grant.accessed = now;
+		clear_expired_grant_tokens(grant); -- Clear expired tokens while we're here
+		token_store:set_key(token_user, token_id, grant);
+	end
+
+	token_info.id = token_id;
+	token_info.grant = grant;
+	token_info.jid = grant.jid;
+
+	return token_info;
 end
 
+
 function get_token_info(token)
 	local token_id, token_user, token_host, token_secret = parse_token(token);
 	if not token_id then
@@ -238,7 +216,6 @@
 	};
 end
 
-
 function revoke_token(token)
 	local token_id, token_user, token_host = parse_token(token);
 	if not token_id then
@@ -258,7 +235,7 @@
 			module:log("debug", "SASL handler failed to verify token: %s", err);
 			return nil, nil, extra;
 		end
-		local token_user, token_host, resource = jid.split(token_info.jid);
+		local token_user, token_host, resource = jid.split(token_info.grant.jid);
 		if realm ~= token_host or (purpose and token_info.purpose ~= purpose) then
 			return nil, nil, extra;
 		end