Changeset

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
parents 6316:0bd63c52bbed
children 6318:fe797da37174
files mod_http_oauth2/README.md mod_http_oauth2/mod_http_oauth2.lua
diffstat 2 files changed, 24 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- a/mod_http_oauth2/README.md	Tue Jul 01 23:45:02 2025 -0500
+++ b/mod_http_oauth2/README.md	Wed Jul 02 15:53:02 2025 +0200
@@ -292,6 +292,8 @@
 OpenID scopes such as `openid` and `profile` can be used for "Login
 with XMPP" without granting access to more than limited profile details.
 
+The `offline_access` scope must be requested to receive refresh tokens.
+
 ## Compatibility
 
 Requires Prosody trunk (April 2023), **not** compatible with Prosody 0.12 or
--- a/mod_http_oauth2/mod_http_oauth2.lua	Tue Jul 01 23:45:02 2025 -0500
+++ b/mod_http_oauth2/mod_http_oauth2.lua	Wed Jul 02 15:53:02 2025 +0200
@@ -28,6 +28,15 @@
 	end
 end
 
+local function array_contains(haystack, needle)
+	for _, item in ipairs(haystack) do
+		if item == needle then
+			return true
+		end
+	end
+	return false
+end
+
 local function strict_url_parse(urlstr)
 	local url_parts = url.parse(urlstr);
 	if not url_parts then return url_parts; end
@@ -175,8 +184,13 @@
 end
 
 local openid_claims = set.new();
+
 module:add_item("openid-claim", "openid");
 
+-- https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
+-- The "offline_access" scope grants access to refresh tokens
+module:add_item("openid-claim", "offline_access");
+
 module:handle_items("openid-claim", function(event)
 	authorization_server_metadata = nil;
 	openid_claims:add(event.item);
@@ -316,7 +330,10 @@
 		end
 	end
 	-- in with the new refresh token
-	local refresh_token = refresh_token_info ~= false and tokens.create_token(token_jid, grant.id, nil, default_refresh_ttl, "oauth2-refresh");
+	local refresh_token;
+	if refresh_token_info ~= false and array_contains(parse_scopes(scope_string), "offline_access") then
+		refresh_token = tokens.create_token(token_jid, grant.id, nil, default_refresh_ttl, "oauth2-refresh");
+	end
 
 	if role == "xmpp" then
 		-- Special scope meaning the users default role.
@@ -861,15 +878,6 @@
 	end
 end
 
-local function array_contains(haystack, needle)
-	for _, item in ipairs(haystack) do
-		if item == needle then
-			return true
-		end
-	end
-	return false
-end
-
 function handle_token_grant(event)
 	local credentials = get_request_credentials(event.request);
 
@@ -988,6 +996,10 @@
 		roles = user_assumable_roles(auth_state.user.username, roles);
 
 		if not prompt:contains("consent") then
+			if array_contains(scopes, "offline_access") then
+				-- MUST ensure that the prompt parameter contains consent
+				return error_response(request, redirect_uri, oauth_error("consent_required"));
+			end
 			local grants = tokens.get_user_grants(auth_state.user.username);
 			local matching_grant;
 			if grants then