# HG changeset patch # User Kim Alvefur # Date 1699999417 -3600 # Node ID 8488ebde57398db9254198d437b8dd1cdd8146d8 # Parent c77010f25b144a3a4398e91b3848c62a100ea42b mod_http_oauth2: Skip consent screen if requested by client and same scopes already granted This follows the intent behind the OpenID Connect 'prompt' parameter when it does not include the 'consent' keyword, that is the client wishes to skip the consent screen. If the user has already granted the exact same scopes to the exact same client in the past, then one can assume that they may grant it again. diff -r c77010f25b14 -r 8488ebde5739 mod_http_oauth2/mod_http_oauth2.lua --- a/mod_http_oauth2/mod_http_oauth2.lua Tue Nov 14 16:01:33 2023 +0100 +++ b/mod_http_oauth2/mod_http_oauth2.lua Tue Nov 14 23:03:37 2023 +0100 @@ -111,7 +111,9 @@ local registration_options = module:get_option("oauth2_registration_options", { default_ttl = registration_ttl; accept_expired = not registration_ttl }); +-- Flip these for Extra Security! local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false); +local respect_prompt = module:get_option_boolean("oauth2_respect_oidc_prompt", true); local verification_key; local sign_client, verify_client; @@ -861,37 +863,56 @@ end -- The 'prompt' parameter from OpenID Core - local prompt = set.new(parse_scopes(params.prompt or "select_account login consent")); - if prompt:contains("none") then - -- Client wants no interaction, only confirmation of prior login and - -- consent, but this is not implemented. - return error_response(request, redirect_uri, oauth_error("interaction_required")); - elseif not prompt:contains("select_account") and not params.login_hint then - -- TODO If the login page is split into account selection followed by login - -- (e.g. password), and then the account selection could be skipped iff the - -- 'login_hint' parameter is present. - return error_response(request, redirect_uri, oauth_error("account_selection_required")); - elseif not prompt:contains("login") then - -- Currently no cookies or such are used, so login is required every time. - return error_response(request, redirect_uri, oauth_error("login_required")); - elseif not prompt:contains("consent") then - -- Are there any circumstances when consent would be implied or assumed? - return error_response(request, redirect_uri, oauth_error("consent_required")); - end + local prompt = set.new(parse_scopes(respect_prompt and params.prompt or "select_account login consent")); local auth_state = get_auth_state(request); if not auth_state.user then + if not prompt:contains("login") then + -- Currently no cookies or such are used, so login is required every time. + return error_response(request, redirect_uri, oauth_error("login_required")); + end + -- Render login page local extra = {}; if params.login_hint then extra.username_hint = (jid.prepped_split(params.login_hint)); + elseif not prompt:contains("select_account") then + -- TODO If the login page is split into account selection followed by login + -- (e.g. password), and then the account selection could be skipped iff the + -- 'login_hint' parameter is present. + return error_response(request, redirect_uri, oauth_error("account_selection_required")); end return render_page(templates.login, { state = auth_state; client = client; extra = extra }); elseif auth_state.consent == nil then - -- Render consent page local scopes, roles = split_scopes(requested_scopes); roles = user_assumable_roles(auth_state.user.username, roles); - return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true); + + if not prompt:contains("consent") then + local grants = tokens.get_user_grants(auth_state.user.username); + local matching_grant; + if grants then + for grant_id, grant in pairs(grants) do + if grant.data and grant.data.oauth2_client and grant.data.oauth2_client.hash == client.client_hash then + if set.new(parse_scopes(grant.data.oauth2_scopes)) == set.new(scopes+roles) then + matching_grant = grant_id; + break + end + end + end + end + + if not matching_grant then + return error_response(request, redirect_uri, oauth_error("consent_required")); + else + -- Consent for these scopes already granted to this exact client, continue + auth_state.scopes = scopes + roles; + auth_state.consent = "granted"; + end + + else + -- Render consent page + return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true); + end elseif not auth_state.consent then -- Notify client of rejection if redirect_uri == device_uri then