# HG changeset patch # User Kim Alvefur # Date 1751464382 -7200 # Node ID 8108aec64fb9846bd73a1961e4310c49b46ec0bf # Parent 0bd63c52bbedb2ed8091b0d4496f59b45d8d206c 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. diff -r 0bd63c52bbed -r 8108aec64fb9 mod_http_oauth2/README.md --- 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 diff -r 0bd63c52bbed -r 8108aec64fb9 mod_http_oauth2/mod_http_oauth2.lua --- 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