Software / code / prosody-modules
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