discourse/lib/discourse_connect_provider.rb
Sam 020c77440e
FEATURE: allow for overlapping DiscourseConnect secrets per domain (#16915)
Previously we limited Discourse Connect provider to 1 secret per domain.

This made it pretty awkward to cycle secrets in environments where config
takes time to propagate

This change allows for the same domain to have multiple secrets

Also fixes internal implementation on DiscourseConnectProvider which was
not thread safe as it leaned on class variables to ferry data around

Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
Co-authored-by: David Taylor <david@taylorhq.com>
2022-05-31 15:24:04 +10:00

63 lines
2.0 KiB
Ruby

# frozen_string_literal: true
class DiscourseConnectProvider < DiscourseConnectBase
class BlankSecret < RuntimeError; end
class BlankReturnUrl < RuntimeError; end
def self.parse(payload, sso_secret = nil, **init_kwargs)
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
raise BlankSecret
end
super(payload, sso_secret, **init_kwargs)
end
def self.lookup_return_sso_url(parsed_payload)
decoded = Base64.decode64(parsed_payload["sso"])
decoded_hash = Rack::Utils.parse_query(decoded)
decoded_hash['return_sso_url']
end
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
provider_secrets = SiteSetting
.discourse_connect_provider_secrets
.split("\n")
.map { |row| row.split("|", 2) }
.sort_by { |k, _| k }
.reverse
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"]
end
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
end
end