Mercurial > prosody-modules
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 |