Software /
code /
prosody
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