Comparison

plugins/mod_tokenauth.lua @ 12997:0a56b84ec4ad

mod_tokenauth: Support for creating sub-tokens Properties of sub-tokens: - They share the same id as their parent token - Sub-tokens may not have their own sub-tokens (but may have sibling tokens) - They always have the same or shorter lifetime compared to their parent token - Revoking a parent token revokes all sub-tokens - Sub-tokens always have the same JID as the parent token - They do not have their own 'accessed' property - accessing a sub-token updates the parent token's accessed time Although this is a generic API, it is designed to at least fill the needs of OAuth2 refresh + access tokens (where the parent token is the refresh token and the sub-tokens are access tokens).
author Matthew Wild <mwild1@gmail.com>
date Sun, 26 Mar 2023 16:46:48 +0100
parent 12996:e8716515405e
child 12998:601d9a375b86
comparison
equal deleted inserted replaced
12996:e8716515405e 12997:0a56b84ec4ad
61 if not ok then 61 if not ok then
62 return nil, err; 62 return nil, err;
63 end 63 end
64 64
65 return token, token_info; 65 return token, token_info;
66 end
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
66 end 134 end
67 135
68 local function parse_token(encoded_token) 136 local function parse_token(encoded_token)
69 if not encoded_token then return nil; end 137 if not encoded_token then return nil; end
70 local encoded_data = encoded_token:match("^secret%-token:(.+)$"); 138 local encoded_data = encoded_token:match("^secret%-token:(.+)$");
75 if not token_id then return nil; end 143 if not token_id then return nil; end
76 local token_user, token_host = jid.split(token_jid); 144 local token_user, token_host = jid.split(token_jid);
77 return token_id, token_user, token_host, token_secret; 145 return token_id, token_user, token_host, token_secret;
78 end 146 end
79 147
148 local function _validate_token_info(token_user, token_id, token_info, sub_token_info)
149 local now = os.time();
150 if token_info.expires and token_info.expires < now then
151 if token_info.type == "subtoken" then
152 clear_expired_sub_tokens(token_user, token_id);
153 else
154 token_store:set(token_user, token_id, nil);
155 end
156 return nil, "not-authorized";
157 end
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
182
80 local function _get_validated_token_info(token_id, token_user, token_host, token_secret) 183 local function _get_validated_token_info(token_id, token_user, token_host, token_secret)
81 if token_host ~= module.host then 184 if token_host ~= module.host then
82 return nil, "invalid-host"; 185 return nil, "invalid-host";
83 end 186 end
84 187
92 token_store:set(token_user, token_id, nil); 195 token_store:set(token_user, token_id, nil);
93 return nil, "not-authorized"; 196 return nil, "not-authorized";
94 end 197 end
95 198
96 -- Check provided secret 199 -- Check provided secret
97 if not hashes.equals(hashes.sha256(token_secret, true), token.secret_sha256) then 200 local secret_hash = hashes.sha256(token_secret, true);
98 return nil, "not-authorized"; 201 if not hashes.equals(secret_hash, token.secret_sha256) then
99 end 202 local sub_token_info = token.sub_tokens and token.sub_tokens[secret_hash];
100 203 if sub_token_info then
101 local token_info = token.token_info; 204 return _validate_token_info(token_user, token_id, token.token_info, sub_token_info);
102 205 end
103 local now = os.time(); 206 return nil, "not-authorized";
104 if token_info.expires and token_info.expires < now then 207 end
105 token_store:set(token_user, token_id, nil); 208
106 return nil, "not-authorized"; 209 return _validate_token_info(token_user, token_id, token.token_info);
107 end 210
108
109 local account_info = usermanager.get_account_info(token_user, module.host);
110 local password_updated_at = account_info and account_info.password_updated;
111 if password_updated_at and password_updated_at > token_info.created then
112 token_store:set(token_user, token_id, nil);
113 return nil, "not-authorized";
114 end
115
116 local last_accessed = token_info.accessed;
117 if not last_accessed or (now - last_accessed) > access_time_granularity then
118 token_info.accessed = now;
119 token_store:set(token_user, token_id, token_info);
120 end
121
122 return token_info
123 end 211 end
124 212
125 function get_token_info(token) 213 function get_token_info(token)
126 local token_id, token_user, token_host, token_secret = parse_token(token); 214 local token_id, token_user, token_host, token_secret = parse_token(token);
127 if not token_id then 215 if not token_id then