diff options
Diffstat (limited to 'plugins/mod_tokenauth.lua')
-rw-r--r-- | plugins/mod_tokenauth.lua | 350 |
1 files changed, 311 insertions, 39 deletions
diff --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua index c04a1aa4..95b0f8d6 100644 --- a/plugins/mod_tokenauth.lua +++ b/plugins/mod_tokenauth.lua @@ -1,82 +1,354 @@ -local id = require "util.id"; -local jid = require "util.jid"; -local base64 = require "util.encodings".base64; +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", "map"); +local token_store = module:open_store("auth_tokens", "keyval+"); -function create_jid_token(actor_jid, token_jid, token_scope, token_ttl) - token_jid = jid.prep(token_jid); - if not actor_jid or token_jid ~= actor_jid and not jid.compare(token_jid, actor_jid) then +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 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 - local token_info = { + local grant_id = id.short(); + local now = os.time(); + + local grant = { + id = grant_id; + owner = actor_jid; - created = os.time(); - expires = token_ttl and (os.time() + token_ttl) or nil; - jid = token_jid; - session = { - username = token_username; - host = token_host; - resource = token_resource; - - auth_scope = token_scope; - }; + 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_id = id.long(); - local token = base64.encode("1;"..jid.join(token_username, token_host)..";"..token_id); - token_store:set(token_username, token_id, token_info); + 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 - return token, token_info; + 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) - local token = base64.decode(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_jid, token_id = token:match("^1;([^;]+);(.+)$"); - if not token_jid 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; + return token_id, token_user, token_host, token_secret; end -function get_token_info(token) - local token_id, token_user, token_host = parse_token(token); - if not token_id then - return nil, "invalid-token-format"; +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 token_info, err = token_store:get(token_user, token_id); - if not token_info 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 + 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 - if token_info.expires and token_info.expires < os.time() then + -- Verify grant validity (expiry, etc.) + grant = _get_validated_grant_info(token_user, grant); + if not grant then return nil, "not-authorized"; end - return token_info + -- 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 revoke_token(token) - local token_id, token_user, token_host = parse_token(token); +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 - return token_store:set(token_user, token_id, nil); + 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) |