comparison mod_http_oauth2/mod_http_oauth2.lua @ 5715:8488ebde5739

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.
author Kim Alvefur <zash@zash.se>
date Tue, 14 Nov 2023 23:03:37 +0100
parents 527c747711f3
children 426c42c11f89
comparison
equal deleted inserted replaced
5714:c77010f25b14 5715:8488ebde5739
109 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256"); 109 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256");
110 local registration_ttl = module:get_option("oauth2_registration_ttl", nil); 110 local registration_ttl = module:get_option("oauth2_registration_ttl", nil);
111 local registration_options = module:get_option("oauth2_registration_options", 111 local registration_options = module:get_option("oauth2_registration_options",
112 { default_ttl = registration_ttl; accept_expired = not registration_ttl }); 112 { default_ttl = registration_ttl; accept_expired = not registration_ttl });
113 113
114 -- Flip these for Extra Security!
114 local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false); 115 local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false);
116 local respect_prompt = module:get_option_boolean("oauth2_respect_oidc_prompt", true);
115 117
116 local verification_key; 118 local verification_key;
117 local sign_client, verify_client; 119 local sign_client, verify_client;
118 if registration_key then 120 if registration_key then
119 -- Tie it to the host if global 121 -- Tie it to the host if global
859 return client_scopes:contains(scope); 861 return client_scopes:contains(scope);
860 end); 862 end);
861 end 863 end
862 864
863 -- The 'prompt' parameter from OpenID Core 865 -- The 'prompt' parameter from OpenID Core
864 local prompt = set.new(parse_scopes(params.prompt or "select_account login consent")); 866 local prompt = set.new(parse_scopes(respect_prompt and params.prompt or "select_account login consent"));
865 if prompt:contains("none") then
866 -- Client wants no interaction, only confirmation of prior login and
867 -- consent, but this is not implemented.
868 return error_response(request, redirect_uri, oauth_error("interaction_required"));
869 elseif not prompt:contains("select_account") and not params.login_hint then
870 -- TODO If the login page is split into account selection followed by login
871 -- (e.g. password), and then the account selection could be skipped iff the
872 -- 'login_hint' parameter is present.
873 return error_response(request, redirect_uri, oauth_error("account_selection_required"));
874 elseif not prompt:contains("login") then
875 -- Currently no cookies or such are used, so login is required every time.
876 return error_response(request, redirect_uri, oauth_error("login_required"));
877 elseif not prompt:contains("consent") then
878 -- Are there any circumstances when consent would be implied or assumed?
879 return error_response(request, redirect_uri, oauth_error("consent_required"));
880 end
881 867
882 local auth_state = get_auth_state(request); 868 local auth_state = get_auth_state(request);
883 if not auth_state.user then 869 if not auth_state.user then
870 if not prompt:contains("login") then
871 -- Currently no cookies or such are used, so login is required every time.
872 return error_response(request, redirect_uri, oauth_error("login_required"));
873 end
874
884 -- Render login page 875 -- Render login page
885 local extra = {}; 876 local extra = {};
886 if params.login_hint then 877 if params.login_hint then
887 extra.username_hint = (jid.prepped_split(params.login_hint)); 878 extra.username_hint = (jid.prepped_split(params.login_hint));
879 elseif not prompt:contains("select_account") then
880 -- TODO If the login page is split into account selection followed by login
881 -- (e.g. password), and then the account selection could be skipped iff the
882 -- 'login_hint' parameter is present.
883 return error_response(request, redirect_uri, oauth_error("account_selection_required"));
888 end 884 end
889 return render_page(templates.login, { state = auth_state; client = client; extra = extra }); 885 return render_page(templates.login, { state = auth_state; client = client; extra = extra });
890 elseif auth_state.consent == nil then 886 elseif auth_state.consent == nil then
891 -- Render consent page
892 local scopes, roles = split_scopes(requested_scopes); 887 local scopes, roles = split_scopes(requested_scopes);
893 roles = user_assumable_roles(auth_state.user.username, roles); 888 roles = user_assumable_roles(auth_state.user.username, roles);
894 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true); 889
890 if not prompt:contains("consent") then
891 local grants = tokens.get_user_grants(auth_state.user.username);
892 local matching_grant;
893 if grants then
894 for grant_id, grant in pairs(grants) do
895 if grant.data and grant.data.oauth2_client and grant.data.oauth2_client.hash == client.client_hash then
896 if set.new(parse_scopes(grant.data.oauth2_scopes)) == set.new(scopes+roles) then
897 matching_grant = grant_id;
898 break
899 end
900 end
901 end
902 end
903
904 if not matching_grant then
905 return error_response(request, redirect_uri, oauth_error("consent_required"));
906 else
907 -- Consent for these scopes already granted to this exact client, continue
908 auth_state.scopes = scopes + roles;
909 auth_state.consent = "granted";
910 end
911
912 else
913 -- Render consent page
914 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true);
915 end
895 elseif not auth_state.consent then 916 elseif not auth_state.consent then
896 -- Notify client of rejection 917 -- Notify client of rejection
897 if redirect_uri == device_uri then 918 if redirect_uri == device_uri then
898 local is_device, device_state = verify_device_token(params.state); 919 local is_device, device_state = verify_device_token(params.state);
899 if is_device then 920 if is_device then