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