Software / code / prosody-modules
Comparison
mod_http_oauth2/mod_http_oauth2.lua @ 6317:8108aec64fb9
mod_http_oauth2: Support the "offline_access" for granting refresh tokens
Refresh tokens are no longer included unless this scope is requested and
granted.
BC: This prevents existing implementations that rely on always getting
the refresh token from continuing.
| author | Kim Alvefur <zash@zash.se> |
|---|---|
| date | Wed, 02 Jul 2025 15:53:02 +0200 |
| parent | 6308:e1c54de06905 |
| child | 6318:fe797da37174 |
comparison
equal
deleted
inserted
replaced
| 6316:0bd63c52bbed | 6317:8108aec64fb9 |
|---|---|
| 26 return function(k) | 26 return function(k) |
| 27 return t[k]; | 27 return t[k]; |
| 28 end | 28 end |
| 29 end | 29 end |
| 30 | 30 |
| 31 local function array_contains(haystack, needle) | |
| 32 for _, item in ipairs(haystack) do | |
| 33 if item == needle then | |
| 34 return true | |
| 35 end | |
| 36 end | |
| 37 return false | |
| 38 end | |
| 39 | |
| 31 local function strict_url_parse(urlstr) | 40 local function strict_url_parse(urlstr) |
| 32 local url_parts = url.parse(urlstr); | 41 local url_parts = url.parse(urlstr); |
| 33 if not url_parts then return url_parts; end | 42 if not url_parts then return url_parts; end |
| 34 if url_parts.userinfo then return false; end | 43 if url_parts.userinfo then return false; end |
| 35 if url_parts.port then | 44 if url_parts.port then |
| 173 local function parse_scopes(scope_string) | 182 local function parse_scopes(scope_string) |
| 174 return array(scope_string:gmatch("%S+")); | 183 return array(scope_string:gmatch("%S+")); |
| 175 end | 184 end |
| 176 | 185 |
| 177 local openid_claims = set.new(); | 186 local openid_claims = set.new(); |
| 187 | |
| 178 module:add_item("openid-claim", "openid"); | 188 module:add_item("openid-claim", "openid"); |
| 189 | |
| 190 -- https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess | |
| 191 -- The "offline_access" scope grants access to refresh tokens | |
| 192 module:add_item("openid-claim", "offline_access"); | |
| 179 | 193 |
| 180 module:handle_items("openid-claim", function(event) | 194 module:handle_items("openid-claim", function(event) |
| 181 authorization_server_metadata = nil; | 195 authorization_server_metadata = nil; |
| 182 openid_claims:add(event.item); | 196 openid_claims:add(event.item); |
| 183 end, function() | 197 end, function() |
| 314 module:log("error", "Could not revoke refresh token: %s", err); | 328 module:log("error", "Could not revoke refresh token: %s", err); |
| 315 return 500; | 329 return 500; |
| 316 end | 330 end |
| 317 end | 331 end |
| 318 -- in with the new refresh token | 332 -- in with the new refresh token |
| 319 local refresh_token = refresh_token_info ~= false and tokens.create_token(token_jid, grant.id, nil, default_refresh_ttl, "oauth2-refresh"); | 333 local refresh_token; |
| 334 if refresh_token_info ~= false and array_contains(parse_scopes(scope_string), "offline_access") then | |
| 335 refresh_token = tokens.create_token(token_jid, grant.id, nil, default_refresh_ttl, "oauth2-refresh"); | |
| 336 end | |
| 320 | 337 |
| 321 if role == "xmpp" then | 338 if role == "xmpp" then |
| 322 -- Special scope meaning the users default role. | 339 -- Special scope meaning the users default role. |
| 323 local user_default_role = usermanager.get_user_role(jid.node(token_jid), module.host); | 340 local user_default_role = usermanager.get_user_role(jid.node(token_jid), module.host); |
| 324 role = user_default_role and user_default_role.name; | 341 role = user_default_role and user_default_role.name; |
| 859 else | 876 else |
| 860 module:log("debug", "Challenge method %q enabled", handler_type); | 877 module:log("debug", "Challenge method %q enabled", handler_type); |
| 861 end | 878 end |
| 862 end | 879 end |
| 863 | 880 |
| 864 local function array_contains(haystack, needle) | |
| 865 for _, item in ipairs(haystack) do | |
| 866 if item == needle then | |
| 867 return true | |
| 868 end | |
| 869 end | |
| 870 return false | |
| 871 end | |
| 872 | |
| 873 function handle_token_grant(event) | 881 function handle_token_grant(event) |
| 874 local credentials = get_request_credentials(event.request); | 882 local credentials = get_request_credentials(event.request); |
| 875 | 883 |
| 876 event.response.headers.content_type = "application/json"; | 884 event.response.headers.content_type = "application/json"; |
| 877 event.response.headers.cache_control = "no-store"; | 885 event.response.headers.cache_control = "no-store"; |
| 986 elseif auth_state.consent == nil then | 994 elseif auth_state.consent == nil then |
| 987 local scopes, roles = split_scopes(requested_scopes); | 995 local scopes, roles = split_scopes(requested_scopes); |
| 988 roles = user_assumable_roles(auth_state.user.username, roles); | 996 roles = user_assumable_roles(auth_state.user.username, roles); |
| 989 | 997 |
| 990 if not prompt:contains("consent") then | 998 if not prompt:contains("consent") then |
| 999 if array_contains(scopes, "offline_access") then | |
| 1000 -- MUST ensure that the prompt parameter contains consent | |
| 1001 return error_response(request, redirect_uri, oauth_error("consent_required")); | |
| 1002 end | |
| 991 local grants = tokens.get_user_grants(auth_state.user.username); | 1003 local grants = tokens.get_user_grants(auth_state.user.username); |
| 992 local matching_grant; | 1004 local matching_grant; |
| 993 if grants then | 1005 if grants then |
| 994 for grant_id, grant in pairs(grants) do | 1006 for grant_id, grant in pairs(grants) do |
| 995 if grant.data and grant.data.oauth2_client and grant.data.oauth2_client.hash == client.client_hash then | 1007 if grant.data and grant.data.oauth2_client and grant.data.oauth2_client.hash == client.client_hash then |