Software / code / prosody
Comparison
plugins/mod_tokenauth.lua @ 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 |
| parent | 12997:0a56b84ec4ad |
| child | 12999:c87ac7d1967f |
comparison
equal
deleted
inserted
replaced
| 12997:0a56b84ec4ad | 12998:601d9a375b86 |
|---|---|
| 4 local jid = require "prosody.util.jid"; | 4 local jid = require "prosody.util.jid"; |
| 5 local random = require "prosody.util.random"; | 5 local random = require "prosody.util.random"; |
| 6 local usermanager = require "prosody.core.usermanager"; | 6 local usermanager = require "prosody.core.usermanager"; |
| 7 local generate_identifier = require "prosody.util.id".short; | 7 local generate_identifier = require "prosody.util.id".short; |
| 8 | 8 |
| 9 local token_store = module:open_store("auth_tokens", "map"); | 9 local token_store = module:open_store("auth_tokens", "keyval+"); |
| 10 | 10 |
| 11 local access_time_granularity = module:get_option_number("token_auth_access_time_granularity", 60); | 11 local access_time_granularity = module:get_option_number("token_auth_access_time_granularity", 60); |
| 12 | 12 |
| 13 local function select_role(username, host, role) | 13 local function select_role(username, host, role) |
| 14 if role then | 14 if role then |
| 15 return prosody.hosts[host].authz.get_role_by_name(role); | 15 return prosody.hosts[host].authz.get_role_by_name(role); |
| 16 end | 16 end |
| 17 return usermanager.get_user_role(username, host); | 17 return usermanager.get_user_role(username, host); |
| 18 end | 18 end |
| 19 | 19 |
| 20 function create_jid_token(actor_jid, token_jid, token_role, token_ttl, token_data, token_purpose) | 20 function create_grant(actor_jid, grant_jid, grant_ttl, grant_data) |
| 21 token_jid = jid.prep(token_jid); | 21 grant_jid = jid.prep(grant_jid); |
| 22 if not actor_jid or token_jid ~= actor_jid and not jid.compare(token_jid, actor_jid) then | 22 if not actor_jid or actor_jid ~= grant_jid and not jid.compare(grant_jid, actor_jid) then |
| 23 return nil, "not-authorized"; | 23 module:log("debug", "Actor <%s> is not permitted to create a token granting access to JID <%s>", actor_jid, grant_jid); |
| 24 end | 24 return nil, "not-authorized"; |
| 25 | 25 end |
| 26 local token_username, token_host, token_resource = jid.split(token_jid); | 26 |
| 27 | 27 local grant_username, grant_host, grant_resource = jid.split(grant_jid); |
| 28 if token_host ~= module.host then | 28 |
| 29 return nil, "invalid-host"; | 29 if grant_host ~= module.host then |
| 30 end | 30 return nil, "invalid-host"; |
| 31 | 31 end |
| 32 | |
| 33 local grant_id = id.short(); | |
| 34 local now = os.time(); | |
| 35 | |
| 36 local grant = { | |
| 37 id = grant_id; | |
| 38 | |
| 39 owner = actor_jid; | |
| 40 created = now; | |
| 41 expires = grant_ttl and (now + grant_ttl) or nil; | |
| 42 accessed = now; | |
| 43 | |
| 44 jid = grant_jid; | |
| 45 resource = grant_resource; | |
| 46 | |
| 47 data = grant_data; | |
| 48 | |
| 49 -- tokens[<hash-name>..":"..<secret>] = token_info | |
| 50 tokens = {}; | |
| 51 }; | |
| 52 | |
| 53 local ok, err = token_store:set_key(grant_username, grant_id, grant); | |
| 54 if not ok then | |
| 55 return nil, err; | |
| 56 end | |
| 57 | |
| 58 return grant; | |
| 59 end | |
| 60 | |
| 61 function create_token(grant_jid, grant, token_role, token_ttl, token_purpose, token_data) | |
| 32 if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then | 62 if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then |
| 33 return nil, "bad-request"; | 63 return nil, "bad-request"; |
| 34 end | 64 end |
| 35 | 65 local grant_username, grant_host = jid.split(grant_jid); |
| 36 local token_id = id.short(); | 66 if grant_host ~= module.host then |
| 67 return nil, "invalid-host"; | |
| 68 end | |
| 69 if type(grant) == "string" then -- lookup by id | |
| 70 grant = token_store:get_key(grant_username, grant); | |
| 71 if not grant then return nil; end | |
| 72 end | |
| 73 | |
| 74 if not grant.tokens then return nil, "internal-server-error"; end -- old-style token? | |
| 37 | 75 |
| 38 local now = os.time(); | 76 local now = os.time(); |
| 77 local expires = grant.expires; -- Default to same expiry as grant | |
| 78 if token_ttl then -- explicit lifetime requested | |
| 79 if expires then | |
| 80 -- Grant has an expiry, so limit to that or shorter | |
| 81 expires = math.min(now + token_ttl, expires); | |
| 82 else | |
| 83 -- Grant never expires, just use whatever expiry is requested for the token | |
| 84 expires = now + token_ttl; | |
| 85 end | |
| 86 end | |
| 39 | 87 |
| 40 local token_info = { | 88 local token_info = { |
| 41 id = token_id; | 89 role = token_role; |
| 42 | 90 |
| 43 owner = actor_jid; | |
| 44 created = now; | 91 created = now; |
| 45 expires = token_ttl and (now + token_ttl) or nil; | 92 expires = expires; |
| 46 accessed = now; | |
| 47 jid = token_jid; | |
| 48 purpose = token_purpose; | 93 purpose = token_purpose; |
| 49 | 94 |
| 50 resource = token_resource; | |
| 51 role = token_role; | |
| 52 data = token_data; | 95 data = token_data; |
| 53 }; | 96 }; |
| 54 | 97 |
| 55 local token_secret = random.bytes(18); | 98 local token_secret = random.bytes(18); |
| 56 local token = "secret-token:"..base64.encode("2;"..token_id..";"..token_secret..";"..jid.join(token_username, token_host)); | 99 grant.tokens["sha256:"..hashes.sha256(token_secret, true)] = token_info; |
| 57 local ok, err = token_store:set(token_username, token_id, { | 100 |
| 58 secret_sha256 = hashes.sha256(token_secret, true); | 101 local ok, err = token_store:set_key(grant_username, grant.id, grant); |
| 59 token_info = token_info | |
| 60 }); | |
| 61 if not ok then | 102 if not ok then |
| 62 return nil, err; | 103 return nil, err; |
| 63 end | 104 end |
| 64 | 105 |
| 65 return token, token_info; | 106 local token_string = "secret-token:"..base64.encode("2;"..grant.id..";"..token_secret..";"..grant.jid); |
| 66 end | 107 return token_string, token_info; |
| 67 | |
| 68 function create_sub_token(actor_jid, parent_id, token_role, token_ttl, token_data, token_purpose) | |
| 69 local username, host = jid.split(actor_jid); | |
| 70 if host ~= module.host then | |
| 71 return nil, "invalid-host"; | |
| 72 end | |
| 73 | |
| 74 if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then | |
| 75 return nil, "bad-request"; | |
| 76 end | |
| 77 | |
| 78 -- Find parent token | |
| 79 local parent_token = token_store:get(username, parent_id); | |
| 80 if not parent_token then return nil; end | |
| 81 local token_info = parent_token.token_info; | |
| 82 | |
| 83 local now = os.time(); | |
| 84 local expires = token_info.expires; -- Default to same expiry as parent token | |
| 85 if token_ttl then | |
| 86 if expires then | |
| 87 -- Parent token has an expiry, so limit to that or shorter | |
| 88 expires = math.min(now + token_ttl, expires); | |
| 89 else | |
| 90 -- Parent token never expires, just add whatever expiry is requested | |
| 91 expires = now + token_ttl; | |
| 92 end | |
| 93 end | |
| 94 | |
| 95 local sub_token_info = { | |
| 96 id = parent_id; | |
| 97 type = "subtoken"; | |
| 98 role = token_role or token_info.role; | |
| 99 jid = token_info.jid; | |
| 100 created = now; | |
| 101 expires = expires; | |
| 102 purpose = token_purpose or token_info.purpose; | |
| 103 data = token_data; | |
| 104 }; | |
| 105 | |
| 106 local sub_tokens = parent_token.sub_tokens; | |
| 107 if not sub_tokens then | |
| 108 sub_tokens = {}; | |
| 109 parent_token.sub_tokens = sub_tokens; | |
| 110 end | |
| 111 | |
| 112 local sub_token_secret = random.bytes(18); | |
| 113 sub_tokens[hashes.sha256(sub_token_secret, true)] = sub_token_info; | |
| 114 | |
| 115 local sub_token = "secret-token:"..base64.encode("2;"..token_info.id..";"..sub_token_secret..";"..token_info.jid); | |
| 116 | |
| 117 local ok, err = token_store:set(username, parent_id, parent_token); | |
| 118 if not ok then | |
| 119 return nil, err; | |
| 120 end | |
| 121 | |
| 122 return sub_token, sub_token_info; | |
| 123 end | |
| 124 | |
| 125 local function clear_expired_sub_tokens(username, token_id) | |
| 126 local sub_tokens = token_store:get_key(username, token_id, "sub_tokens"); | |
| 127 if not sub_tokens then return; end | |
| 128 local now = os.time(); | |
| 129 for secret, info in pairs(sub_tokens) do | |
| 130 if info.expires < now then | |
| 131 sub_tokens[secret] = nil; | |
| 132 end | |
| 133 end | |
| 134 end | 108 end |
| 135 | 109 |
| 136 local function parse_token(encoded_token) | 110 local function parse_token(encoded_token) |
| 137 if not encoded_token then return nil; end | 111 if not encoded_token then return nil; end |
| 138 local encoded_data = encoded_token:match("^secret%-token:(.+)$"); | 112 local encoded_data = encoded_token:match("^secret%-token:(.+)$"); |
| 143 if not token_id then return nil; end | 117 if not token_id then return nil; end |
| 144 local token_user, token_host = jid.split(token_jid); | 118 local token_user, token_host = jid.split(token_jid); |
| 145 return token_id, token_user, token_host, token_secret; | 119 return token_id, token_user, token_host, token_secret; |
| 146 end | 120 end |
| 147 | 121 |
| 148 local function _validate_token_info(token_user, token_id, token_info, sub_token_info) | 122 local function clear_expired_grant_tokens(grant, now) |
| 149 local now = os.time(); | 123 local updated; |
| 150 if token_info.expires and token_info.expires < now then | 124 now = now or os.time(); |
| 151 if token_info.type == "subtoken" then | 125 for secret, token_info in pairs(grant.tokens) do |
| 152 clear_expired_sub_tokens(token_user, token_id); | 126 if token_info.expires < now then |
| 153 else | 127 grant.tokens[secret] = nil; |
| 154 token_store:set(token_user, token_id, nil); | 128 updated = true; |
| 155 end | 129 end |
| 156 return nil, "not-authorized"; | 130 end |
| 157 end | 131 return updated; |
| 158 | |
| 159 if token_info.type ~= "subtoken" then | |
| 160 local account_info = usermanager.get_account_info(token_user, module.host); | |
| 161 local password_updated_at = account_info and account_info.password_updated; | |
| 162 if password_updated_at and password_updated_at > token_info.created then | |
| 163 token_store:set(token_user, token_id, nil); | |
| 164 return nil, "not-authorized"; | |
| 165 end | |
| 166 | |
| 167 -- Update last access time if necessary | |
| 168 local last_accessed = token_info.accessed; | |
| 169 if not last_accessed or (now - last_accessed) > access_time_granularity then | |
| 170 token_info.accessed = now; | |
| 171 token_store:set_key(token_user, token_id, "token_info", token_info); | |
| 172 end | |
| 173 end | |
| 174 | |
| 175 if sub_token_info then | |
| 176 -- Parent token validated, now validate (and return) the subtoken | |
| 177 return _validate_token_info(token_user, token_id, sub_token_info); | |
| 178 end | |
| 179 | |
| 180 return token_info | |
| 181 end | 132 end |
| 182 | 133 |
| 183 local function _get_validated_token_info(token_id, token_user, token_host, token_secret) | 134 local function _get_validated_token_info(token_id, token_user, token_host, token_secret) |
| 184 if token_host ~= module.host then | 135 if token_host ~= module.host then |
| 185 return nil, "invalid-host"; | 136 return nil, "invalid-host"; |
| 186 end | 137 end |
| 187 | 138 |
| 188 local token, err = token_store:get(token_user, token_id); | 139 local grant, err = token_store:get_key(token_user, token_id); |
| 189 if not token then | 140 if not grant or not grant.tokens then |
| 190 if err then | 141 if err then |
| 142 module:log("error", "Unable to read from token storage: %s", err); | |
| 191 return nil, "internal-error"; | 143 return nil, "internal-error"; |
| 192 end | 144 end |
| 193 return nil, "not-authorized"; | 145 module:log("warn", "Invalid token in storage (%s / %s)", token_user, token_id); |
| 194 elseif not token.secret_sha256 then -- older token format | |
| 195 token_store:set(token_user, token_id, nil); | |
| 196 return nil, "not-authorized"; | 146 return nil, "not-authorized"; |
| 197 end | 147 end |
| 198 | 148 |
| 199 -- Check provided secret | 149 -- Check provided secret |
| 200 local secret_hash = hashes.sha256(token_secret, true); | 150 local secret_hash = "sha256:"..hashes.sha256(token_secret, true); |
| 201 if not hashes.equals(secret_hash, token.secret_sha256) then | 151 local token_info = grant.tokens[secret_hash]; |
| 202 local sub_token_info = token.sub_tokens and token.sub_tokens[secret_hash]; | 152 if not token_info then |
| 203 if sub_token_info then | 153 module:log("debug", "No tokens matched the given secret"); |
| 204 return _validate_token_info(token_user, token_id, token.token_info, sub_token_info); | 154 return nil, "not-authorized"; |
| 205 end | 155 end |
| 206 return nil, "not-authorized"; | 156 |
| 207 end | 157 -- Check expiry |
| 208 | 158 local now = os.time(); |
| 209 return _validate_token_info(token_user, token_id, token.token_info); | 159 if token_info.expires < now then |
| 210 | 160 module:log("debug", "Token has expired, cleaning it up"); |
| 211 end | 161 grant.tokens[secret_hash] = nil; |
| 162 token_store:set_key(token_user, token_id, grant); | |
| 163 return nil, "not-authorized"; | |
| 164 end | |
| 165 | |
| 166 -- Invalidate grants from before last password change | |
| 167 local account_info = usermanager.get_account_info(token_user, module.host); | |
| 168 local password_updated_at = account_info and account_info.password_updated; | |
| 169 if grant.created < password_updated_at and password_updated_at then | |
| 170 module:log("debug", "Token grant issued before last password change, invalidating it now"); | |
| 171 token_store:set_key(token_user, token_id, nil); | |
| 172 return nil, "not-authorized"; | |
| 173 end | |
| 174 | |
| 175 -- Update last access time if necessary | |
| 176 local last_accessed = grant.accessed; | |
| 177 if not last_accessed or (now - last_accessed) > access_time_granularity then | |
| 178 grant.accessed = now; | |
| 179 clear_expired_grant_tokens(grant); -- Clear expired tokens while we're here | |
| 180 token_store:set_key(token_user, token_id, grant); | |
| 181 end | |
| 182 | |
| 183 token_info.id = token_id; | |
| 184 token_info.grant = grant; | |
| 185 token_info.jid = grant.jid; | |
| 186 | |
| 187 return token_info; | |
| 188 end | |
| 189 | |
| 212 | 190 |
| 213 function get_token_info(token) | 191 function get_token_info(token) |
| 214 local token_id, token_user, token_host, token_secret = parse_token(token); | 192 local token_id, token_user, token_host, token_secret = parse_token(token); |
| 215 if not token_id then | 193 if not token_id then |
| 216 module:log("warn", "Failed to verify access token: %s", token_user); | 194 module:log("warn", "Failed to verify access token: %s", token_user); |
| 235 resource = token_info.resource or resource or generate_identifier(); | 213 resource = token_info.resource or resource or generate_identifier(); |
| 236 | 214 |
| 237 role = select_role(token_user, token_host, token_info.role); | 215 role = select_role(token_user, token_host, token_info.role); |
| 238 }; | 216 }; |
| 239 end | 217 end |
| 240 | |
| 241 | 218 |
| 242 function revoke_token(token) | 219 function revoke_token(token) |
| 243 local token_id, token_user, token_host = parse_token(token); | 220 local token_id, token_user, token_host = parse_token(token); |
| 244 if not token_id then | 221 if not token_id then |
| 245 module:log("warn", "Failed to verify access token: %s", token_user); | 222 module:log("warn", "Failed to verify access token: %s", token_user); |
| 256 local token_info, err = get_token_info(token); | 233 local token_info, err = get_token_info(token); |
| 257 if not token_info then | 234 if not token_info then |
| 258 module:log("debug", "SASL handler failed to verify token: %s", err); | 235 module:log("debug", "SASL handler failed to verify token: %s", err); |
| 259 return nil, nil, extra; | 236 return nil, nil, extra; |
| 260 end | 237 end |
| 261 local token_user, token_host, resource = jid.split(token_info.jid); | 238 local token_user, token_host, resource = jid.split(token_info.grant.jid); |
| 262 if realm ~= token_host or (purpose and token_info.purpose ~= purpose) then | 239 if realm ~= token_host or (purpose and token_info.purpose ~= purpose) then |
| 263 return nil, nil, extra; | 240 return nil, nil, extra; |
| 264 end | 241 end |
| 265 if auth_provider.is_enabled and not auth_provider.is_enabled(token_user) then | 242 if auth_provider.is_enabled and not auth_provider.is_enabled(token_user) then |
| 266 return true, false, token_info; | 243 return true, false, token_info; |