mirror of
https://github.com/bjc/prosody.git
synced 2025-04-01 20:27:39 +03:00
Because the code was using `< now` in a lot of places, things expiring at the current second wouldn't be marked as expired. It isn't noticeable in real-world scenarios but I wanted to create OAuth 2.0 tokens valid for 0 second in integration tests and it wasn't possible. By using `<=` instead of `<`, we make sure tokens don't live a single millisecond more than what they are supposed to.
354 lines
11 KiB
Lua
354 lines
11 KiB
Lua
local base64 = require "prosody.util.encodings".base64;
|
|
local hashes = require "prosody.util.hashes";
|
|
local id = require "prosody.util.id";
|
|
local jid = require "prosody.util.jid";
|
|
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", "keyval+");
|
|
|
|
local access_time_granularity = module:get_option_period("token_auth_access_time_granularity", 60);
|
|
local empty_grant_lifetime = module:get_option_period("tokenless_grant_ttl", "2w");
|
|
|
|
local function select_role(username, host, role_name)
|
|
if not role_name then return end
|
|
local role = usermanager.get_role_by_name(role_name, host);
|
|
if not role then return end
|
|
if not usermanager.user_can_assume_role(username, host, role.name) then return end
|
|
return role;
|
|
end
|
|
|
|
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 grant_username, grant_host, grant_resource = jid.split(grant_jid);
|
|
|
|
if grant_host ~= module.host then
|
|
return nil, "invalid-host";
|
|
end
|
|
|
|
local grant_id = id.short();
|
|
local now = os.time();
|
|
|
|
local grant = {
|
|
id = grant_id;
|
|
|
|
owner = actor_jid;
|
|
created = now;
|
|
expires = grant_ttl and (now + grant_ttl) or nil;
|
|
accessed = now;
|
|
|
|
jid = grant_jid;
|
|
resource = grant_resource;
|
|
|
|
data = grant_data;
|
|
|
|
-- tokens[<hash-name>..":"..<secret>] = token_info
|
|
tokens = {};
|
|
};
|
|
|
|
local ok, err = token_store:set_key(grant_username, grant_id, grant);
|
|
if not ok then
|
|
return nil, err;
|
|
end
|
|
|
|
module:fire_event("token-grant-created", {
|
|
id = grant_id;
|
|
grant = grant;
|
|
username = grant_username;
|
|
host = grant_host;
|
|
});
|
|
|
|
return grant;
|
|
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
|
|
|
|
if not grant.tokens then return nil, "internal-server-error"; end -- old-style token?
|
|
|
|
local now = os.time();
|
|
local expires = grant.expires; -- Default to same expiry as grant
|
|
if token_ttl then -- explicit lifetime requested
|
|
if expires then
|
|
-- Grant has an expiry, so limit to that or shorter
|
|
expires = math.min(now + token_ttl, expires);
|
|
else
|
|
-- Grant never expires, just use whatever expiry is requested for the token
|
|
expires = now + token_ttl;
|
|
end
|
|
end
|
|
|
|
local token_info = {
|
|
role = token_role;
|
|
|
|
created = now;
|
|
expires = expires;
|
|
purpose = token_purpose;
|
|
|
|
data = token_data;
|
|
};
|
|
|
|
local token_secret = random.bytes(18);
|
|
grant.tokens["sha256:"..hashes.sha256(token_secret, true)] = token_info;
|
|
|
|
local ok, err = token_store:set_key(grant_username, grant.id, grant);
|
|
if not ok then
|
|
return nil, err;
|
|
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)
|
|
if not encoded_token then return nil; end
|
|
local encoded_data = encoded_token:match("^secret%-token:(.+)$");
|
|
if not encoded_data then return nil; end
|
|
local token = base64.decode(encoded_data);
|
|
if not token then return nil; end
|
|
local token_id, token_secret, token_jid = token:match("^2;([^;]+);(..................);(.+)$");
|
|
if not token_id then return nil; end
|
|
local token_user, token_host = jid.split(token_jid);
|
|
return token_id, token_user, token_host, token_secret;
|
|
end
|
|
|
|
local function clear_expired_grant_tokens(grant, now)
|
|
local updated;
|
|
now = now or os.time();
|
|
for secret, token_info in pairs(grant.tokens) do
|
|
local expires = token_info.expires;
|
|
if expires and expires <= now then
|
|
grant.tokens[secret] = nil;
|
|
updated = true;
|
|
end
|
|
end
|
|
return updated;
|
|
end
|
|
|
|
local function _get_validated_grant_info(username, grant)
|
|
if type(grant) == "string" then
|
|
grant = token_store:get_key(username, grant);
|
|
end
|
|
if not grant or not grant.created or not grant.id then return nil; end
|
|
|
|
-- Invalidate grants from before last password change
|
|
local account_info = usermanager.get_account_info(username, module.host);
|
|
local password_updated_at = account_info and account_info.password_updated;
|
|
local now = os.time();
|
|
if password_updated_at and grant.created < password_updated_at then
|
|
module:log("debug", "Token grant %s of %s issued before last password change, invalidating it now", grant.id, username);
|
|
token_store:set_key(username, grant.id, nil);
|
|
return nil, "not-authorized";
|
|
elseif grant.expires and grant.expires <= now then
|
|
module:log("debug", "Token grant %s of %s expired, cleaning up", grant.id, username);
|
|
token_store:set_key(username, grant.id, nil);
|
|
return nil, "expired";
|
|
end
|
|
|
|
if not grant.tokens then
|
|
module:log("debug", "Token grant %s of %s without tokens, cleaning up", grant.id, username);
|
|
token_store:set_key(username, grant.id, nil);
|
|
return nil, "invalid";
|
|
end
|
|
|
|
local found_expired = false
|
|
for secret_hash, token_info in pairs(grant.tokens) do
|
|
if token_info.expires and token_info.expires <= now then
|
|
module:log("debug", "Token %s of grant %s of %s has expired, cleaning it up", secret_hash:sub(-8), grant.id, username);
|
|
grant.tokens[secret_hash] = nil;
|
|
found_expired = true;
|
|
end
|
|
end
|
|
|
|
if not grant.expires and next(grant.tokens) == nil and grant.accessed + empty_grant_lifetime <= now then
|
|
module:log("debug", "Token %s of %s grant has no tokens, discarding", grant.id, username);
|
|
token_store:set_key(username, grant.id, nil);
|
|
return nil, "expired";
|
|
elseif found_expired then
|
|
token_store:set_key(username, grant.id, grant);
|
|
end
|
|
|
|
return grant;
|
|
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";
|
|
end
|
|
|
|
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
|
|
module:log("warn", "Invalid token in storage (%s / %s)", token_user, token_id);
|
|
return nil, "not-authorized";
|
|
end
|
|
|
|
-- Check provided secret
|
|
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 and 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
|
|
|
|
-- Verify grant validity (expiry, etc.)
|
|
grant = _get_validated_grant_info(token_user, grant);
|
|
if not grant then
|
|
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_grant_info(username, grant_id)
|
|
local grant = _get_validated_grant_info(username, grant_id);
|
|
if not grant then return nil; end
|
|
|
|
-- Caller is only interested in the grant, no need to expose token stuff to them
|
|
grant.tokens = nil;
|
|
|
|
return grant;
|
|
end
|
|
|
|
function get_user_grants(username)
|
|
local grants = token_store:get(username);
|
|
if not grants then return nil; end
|
|
for grant_id, grant in pairs(grants) do
|
|
grants[grant_id] = _get_validated_grant_info(username, grant);
|
|
end
|
|
return grants;
|
|
end
|
|
|
|
function get_token_info(token)
|
|
local token_id, token_user, token_host, token_secret = parse_token(token);
|
|
if not token_id then
|
|
module:log("warn", "Failed to verify access token: %s", token_user);
|
|
return nil, "invalid-token-format";
|
|
end
|
|
return _get_validated_token_info(token_id, token_user, token_host, token_secret);
|
|
end
|
|
|
|
function get_token_session(token, resource)
|
|
local token_id, token_user, token_host, token_secret = parse_token(token);
|
|
if not token_id then
|
|
module:log("warn", "Failed to verify access token: %s", token_user);
|
|
return nil, "invalid-token-format";
|
|
end
|
|
|
|
local token_info, err = _get_validated_token_info(token_id, token_user, token_host, token_secret);
|
|
if not token_info then return nil, err; end
|
|
|
|
local role = select_role(token_user, token_host, token_info.role);
|
|
if not role then return nil, "not-authorized"; end
|
|
return {
|
|
username = token_user;
|
|
host = token_host;
|
|
resource = token_info.resource or resource or generate_identifier();
|
|
|
|
role = role;
|
|
};
|
|
end
|
|
|
|
function revoke_token(token)
|
|
local grant_id, token_user, token_host, token_secret = parse_token(token);
|
|
if not grant_id then
|
|
module:log("warn", "Failed to verify access token: %s", token_user);
|
|
return nil, "invalid-token-format";
|
|
end
|
|
if token_host ~= module.host then
|
|
return nil, "invalid-host";
|
|
end
|
|
local grant, err = _get_validated_grant_info(token_user, grant_id);
|
|
if not grant then return grant, err; end
|
|
local secret_hash = "sha256:"..hashes.sha256(token_secret, true);
|
|
local token_info = grant.tokens[secret_hash];
|
|
if not grant or not token_info then
|
|
return nil, "item-not-found";
|
|
end
|
|
grant.tokens[secret_hash] = nil;
|
|
local ok, err = token_store:set_key(token_user, grant_id, grant);
|
|
if not ok then
|
|
return nil, err;
|
|
end
|
|
module:fire_event("token-revoked", {
|
|
grant_id = grant_id;
|
|
grant = grant;
|
|
info = token_info;
|
|
username = token_user;
|
|
host = token_host;
|
|
});
|
|
return true;
|
|
end
|
|
|
|
function revoke_grant(username, grant_id)
|
|
local ok, err = token_store:set_key(username, grant_id, nil);
|
|
if not ok then return nil, err; end
|
|
module:fire_event("token-grant-revoked", { id = grant_id, username = username, host = module.host });
|
|
return true;
|
|
end
|
|
|
|
function sasl_handler(auth_provider, purpose, extra)
|
|
return function (sasl, token, realm, _authzid)
|
|
local token_info, err = get_token_info(token);
|
|
if not token_info then
|
|
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.grant.jid);
|
|
if realm ~= token_host or (purpose and token_info.purpose ~= purpose) then
|
|
return nil, nil, extra;
|
|
end
|
|
if auth_provider.is_enabled and not auth_provider.is_enabled(token_user) then
|
|
return true, false, token_info;
|
|
end
|
|
sasl.resource = resource;
|
|
sasl.token_info = token_info;
|
|
return token_user, true, token_info;
|
|
end;
|
|
end
|
|
|
|
module:daily("clear expired grants", function()
|
|
for username in token_store:items() do
|
|
get_user_grants(username); -- clears out expired grants
|
|
end
|
|
end)
|