aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
authorMatthew Wild <mwild1@gmail.com>2023-03-27 18:35:57 +0100
committerMatthew Wild <mwild1@gmail.com>2023-03-27 18:35:57 +0100
commitf4c1f3b35363a30350b06a30b70dfb08b31e2972 (patch)
tree729dd0484adc9894b298ecd878d05a5ce80d5d41 /plugins
parent8743ea29d59256a4a8e4b4a63ad458fb04b512af (diff)
downloadprosody-f4c1f3b35363a30350b06a30b70dfb08b31e2972.tar.gz
prosody-f4c1f3b35363a30350b06a30b70dfb08b31e2972.zip
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.
Diffstat (limited to 'plugins')
-rw-r--r--plugins/mod_tokenauth.lua207
1 files changed, 92 insertions, 115 deletions
diff --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua
index fe10d621..74d68e75 100644
--- a/plugins/mod_tokenauth.lua
+++ b/plugins/mod_tokenauth.lua
@@ -6,7 +6,7 @@ local random = require "prosody.util.random";
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 @@ local function select_role(username, host, role)
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;
- resource = token_resource;
- role = token_role;
- data = token_data;
+ jid = grant_jid;
+ resource = grant_resource;
+
+ 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 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 token_secret = random.bytes(18);
+ grant.tokens["sha256:"..hashes.sha256(token_secret, true)] = token_info;
- 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 @@ local function parse_token(encoded_token)
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";
+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
-
- -- 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
+ return updated;
end
local function _get_validated_token_info(token_id, token_user, token_host, token_secret)
@@ -185,31 +136,58 @@ local function _get_validated_token_info(token_id, token_user, token_host, token
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 @@ function get_token_session(token, resource)
};
end
-
function revoke_token(token)
local token_id, token_user, token_host = parse_token(token);
if not token_id then
@@ -258,7 +235,7 @@ function sasl_handler(auth_provider, purpose, extra)
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