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 |