discourse/lib/second_factor/actions/discourse_connect_provider.rb
Matt Marjanović 619d43ea47
FEATURE: Add prompt=none functionality to SSO Provider protocol (#22393)
This commit adds support for an optional `prompt` parameter in the
payload of the /session/sso_provider endpoint.  If an SSO Consumer
adds a `prompt=none` parameter to the encoded/signed `sso` payload,
then Discourse will avoid trying to login a not-logged-in user:

 * If the user is already logged in, Discourse will immediately
   redirect back to the Consumer with the user's credentials in a
   signed payload, as usual.

 * If the user is not logged in, Discourse will immediately redirect
   back to the Consumer with a signed payload bearing the parameter
   `failed=true`.

This allows the SSO Consumer to simply test whether or not a user is
logged in, without forcing the user to try to log in.  This is useful
when the SSO Consumer allows both anonymous and authenticated access.
(E.g., users that are already logged-in to Discourse can be seamlessly
logged-in to the Consumer site, and anonymous users can remain
anonymous until they explicitly ask to log in.)

This feature is similar to the `prompt=none` functionality in an
OpenID Connect Authentication Request; see
https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
2023-09-28 12:53:28 +01:00

120 lines
3.8 KiB
Ruby

# frozen_string_literal: true
module SecondFactor::Actions
class DiscourseConnectProvider < Base
def skip_second_factor_auth?(params)
sso = get_sso(payload(params))
!current_user || sso.logout || !sso.require_2fa || @opts[:confirmed_2fa_during_login]
end
def second_factor_auth_skipped!(params)
sso = get_sso(payload(params))
return { logout: true, return_sso_url: sso.return_sso_url } if sso.logout
if !current_user
if sso.prompt == "none"
# 'prompt=none' was requested, so just return a failed authentication
# without putting up a login dialog and interrogating the user.
sso.failed = true
return(
{
no_current_user: true,
prompt: sso.prompt,
sso_redirect_url: sso.to_url(sso.return_sso_url),
}
)
end
# ...otherwise, trigger the usual redirect to login dialog.
return { no_current_user: true }
end
populate_user_data(sso)
sso.confirmed_2fa = true if @opts[:confirmed_2fa_during_login]
{ sso_redirect_url: sso.to_url(sso.return_sso_url) }
end
def no_second_factors_enabled!(params)
sso = get_sso(payload(params))
populate_user_data(sso)
sso.no_2fa_methods = true
{ sso_redirect_url: sso.to_url(sso.return_sso_url) }
end
def second_factor_auth_required!(params)
pl = payload(params)
sso = get_sso(pl)
hostname = URI(sso.return_sso_url).hostname
{
callback_params: {
payload: pl,
},
callback_path: session_sso_provider_path,
callback_method: "GET",
description:
I18n.t(
"second_factor_auth.actions.discourse_connect_provider.description",
hostname: hostname,
),
}
end
def second_factor_auth_completed!(callback_params)
sso = get_sso(callback_params[:payload])
populate_user_data(sso)
sso.confirmed_2fa = true
{ sso_redirect_url: sso.to_url(sso.return_sso_url) }
end
private
def payload(params)
return @opts[:payload] if @opts[:payload]
params.require(:sso)
request.query_string
end
def populate_user_data(sso)
sso.name = current_user.name
sso.username = current_user.username
sso.email = current_user.email
sso.external_id = current_user.id.to_s
sso.admin = current_user.admin?
sso.moderator = current_user.moderator?
sso.groups = current_user.groups.pluck(:name).join(",")
if current_user.uploaded_avatar.present?
base_url =
Discourse.store.external? ? "#{Discourse.store.absolute_base_url}/" : Discourse.base_url
avatar_url =
"#{base_url}#{Discourse.store.get_path_for_upload(current_user.uploaded_avatar)}"
sso.avatar_url = UrlHelper.absolute Discourse.store.cdn_url(avatar_url)
end
if current_user.user_profile.profile_background_upload.present?
sso.profile_background_url =
UrlHelper.absolute(
GlobalPath.upload_cdn_path(current_user.user_profile.profile_background_upload.url),
)
end
if current_user.user_profile.card_background_upload.present?
sso.card_background_url =
UrlHelper.absolute(
GlobalPath.upload_cdn_path(current_user.user_profile.card_background_upload.url),
)
end
end
def get_sso(payload)
sso = ::DiscourseConnectProvider.parse(payload)
raise ::DiscourseConnectProvider::BlankReturnUrl.new if sso.return_sso_url.blank?
sso
rescue ::DiscourseConnectProvider::ParseError => e
if SiteSetting.verbose_discourse_connect_logging
Rails.logger.warn(
"Verbose SSO log: Signature parse error\n\n#{e.message}\n\n#{sso&.diagnostics}",
)
end
raise
end
end
end