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