2019-05-02 18:17:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2022-01-06 07:28:46 -05:00
|
|
|
class DiscourseConnectProvider < DiscourseConnectBase
|
2019-07-26 10:37:23 -04:00
|
|
|
class BlankSecret < RuntimeError
|
|
|
|
end
|
2022-04-13 08:04:09 -04:00
|
|
|
class BlankReturnUrl < RuntimeError
|
|
|
|
end
|
2023-09-28 07:53:28 -04:00
|
|
|
class InvalidParameterValueError < RuntimeError
|
|
|
|
attr_reader :param
|
|
|
|
def initialize(param)
|
|
|
|
@param = param
|
|
|
|
super("Invalid value for parameter `#{param}`")
|
|
|
|
end
|
|
|
|
end
|
2018-12-19 04:22:10 -05:00
|
|
|
|
2022-05-31 01:24:04 -04:00
|
|
|
def self.parse(payload, sso_secret = nil, **init_kwargs)
|
2023-09-28 07:53:28 -04:00
|
|
|
# We extract the return_sso_url parameter early; we need the URL's host
|
|
|
|
# in order to lookup the correct SSO secret in our site settings.
|
2022-05-31 01:24:04 -04:00
|
|
|
parsed_payload = Rack::Utils.parse_query(payload)
|
|
|
|
return_sso_url = lookup_return_sso_url(parsed_payload)
|
|
|
|
|
|
|
|
raise ParseError if !return_sso_url
|
|
|
|
|
|
|
|
sso_secret ||= lookup_sso_secret(return_sso_url, parsed_payload)
|
|
|
|
|
|
|
|
if sso_secret.blank?
|
|
|
|
begin
|
|
|
|
host = URI.parse(return_sso_url).host
|
|
|
|
Rails.logger.warn(
|
|
|
|
"SSO failed; website #{host} is not in the `discourse_connect_provider_secrets` site settings",
|
|
|
|
)
|
|
|
|
rescue StandardError => e
|
|
|
|
# going for StandardError cause URI::Error may not be enough, eg it parses to something not
|
|
|
|
# responding to host
|
|
|
|
Discourse.warn_exception(
|
|
|
|
e,
|
|
|
|
message: "SSO failed; invalid or missing return_sso_url in SSO payload",
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2019-07-26 10:37:23 -04:00
|
|
|
raise BlankSecret
|
|
|
|
end
|
2018-12-19 04:22:10 -05:00
|
|
|
|
2023-09-28 07:53:28 -04:00
|
|
|
sso = super(payload, sso_secret, **init_kwargs)
|
|
|
|
|
|
|
|
# Do general parameter validation now, after signature-verification has succeeded.
|
|
|
|
raise InvalidParameterValueError.new("prompt") if (sso.prompt != nil) && (sso.prompt != "none")
|
|
|
|
|
|
|
|
sso
|
2018-12-19 04:22:10 -05:00
|
|
|
end
|
|
|
|
|
2022-05-31 01:24:04 -04:00
|
|
|
def self.lookup_return_sso_url(parsed_payload)
|
|
|
|
decoded = Base64.decode64(parsed_payload["sso"])
|
2018-12-19 04:22:10 -05:00
|
|
|
decoded_hash = Rack::Utils.parse_query(decoded)
|
2022-05-31 01:24:04 -04:00
|
|
|
decoded_hash["return_sso_url"]
|
2018-12-19 04:22:10 -05:00
|
|
|
end
|
|
|
|
|
2022-05-31 01:24:04 -04:00
|
|
|
def self.lookup_sso_secret(return_sso_url, parsed_payload)
|
|
|
|
return nil unless return_sso_url && SiteSetting.enable_discourse_connect_provider
|
|
|
|
|
|
|
|
return_url_host = URI.parse(return_sso_url).host
|
2018-12-19 04:22:10 -05:00
|
|
|
|
2022-05-31 01:24:04 -04:00
|
|
|
provider_secrets =
|
|
|
|
SiteSetting
|
|
|
|
.discourse_connect_provider_secrets
|
|
|
|
.split("\n")
|
|
|
|
.map { |row| row.split("|", 2) }
|
|
|
|
.sort_by { |k, _| k }
|
|
|
|
.reverse
|
2018-12-19 04:22:10 -05:00
|
|
|
|
2022-05-31 01:24:04 -04:00
|
|
|
first_domain_match = nil
|
|
|
|
|
|
|
|
pair =
|
|
|
|
provider_secrets.find do |domain, configured_secret|
|
|
|
|
if WildcardDomainChecker.check_domain(domain, return_url_host)
|
|
|
|
first_domain_match ||= configured_secret
|
|
|
|
sign(parsed_payload["sso"], configured_secret) == parsed_payload["sig"]
|
2023-01-09 07:10:19 -05:00
|
|
|
end
|
2022-05-31 01:24:04 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# falls back to a secret which will fail to validate in DiscourseConnectBase
|
|
|
|
# this ensures error flow is correct
|
|
|
|
pair.present? ? pair[1] : first_domain_match
|
2018-12-19 04:22:10 -05:00
|
|
|
end
|
|
|
|
end
|