You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/app/controllers/sso_settings_controller.rb

133 lines
5.3 KiB

# frozen_string_literal: true
class SsoSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, only: [:show, :update]
def show; end
def update
saml_config = params[:saml_config] || {}
# Handle IdP metadata file upload and parsing
if params[:idp_metadata_file].present?
begin
parsed_config = parse_idp_metadata(params[:idp_metadata_file])
saml_config.merge!(parsed_config)
# Save the parsed configuration immediately
@encrypted_config.value = saml_config.to_json
if @encrypted_config.save
redirect_to settings_sso_path, notice: 'IdP metadata parsed and saved successfully!'
else
redirect_to settings_sso_path, alert: 'Failed to save parsed configuration. Please try again.'
end
return
rescue StandardError => e
Rails.logger.error "Failed to parse IdP metadata: #{e.message}"
redirect_to settings_sso_path, alert: "Failed to parse IdP metadata: #{e.message}"
return
end
end
# Validate required fields for manual configuration
if saml_config['idp_sso_service_url'].blank? || saml_config['idp_cert_fingerprint'].blank?
redirect_to settings_sso_path, alert: 'Please fill in all required SAML configuration fields.'
return
end
# Save the SAML configuration
@encrypted_config.value = saml_config.to_json
if @encrypted_config.save
redirect_to settings_sso_path, notice: 'SAML configuration saved successfully.'
else
redirect_to settings_sso_path, alert: 'Failed to save SAML configuration. Please try again.'
end
rescue StandardError => e
Rails.logger.error "Failed to save SAML config: #{e.message}"
redirect_to settings_sso_path, alert: 'An error occurred while saving the configuration.'
end
private
def load_encrypted_config
@encrypted_config =
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'saml_configs')
end
def parse_idp_metadata(metadata_file)
require 'nokogiri'
# Read and parse the XML metadata file
xml_content = metadata_file.read
doc = Nokogiri::XML(xml_content)
# Remove default namespace to make XPath queries simpler
doc.remove_namespaces!
config = {}
# Extract Entity ID
entity_descriptor = doc.at_xpath('//EntityDescriptor')
config['idp_entity_id'] = entity_descriptor['entityID'] if entity_descriptor
# Try SAML 2.0 SSO Service URL (Azure AD puts this in IDPSSODescriptor)
sso_service = doc.at_xpath('//IDPSSODescriptor/SingleSignOnService[@Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"]')
sso_service ||= doc.at_xpath('//IDPSSODescriptor/SingleSignOnService[@Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"]')
if sso_service
config['idp_sso_service_url'] = sso_service['Location']
else
# Fallback: Try WS-Federation endpoints and convert to SAML
wsfed_endpoint = doc.at_xpath('//SecurityTokenServiceEndpoint/EndpointReference/Address')
wsfed_endpoint ||= doc.at_xpath('//PassiveRequestorEndpoint/EndpointReference/Address')
if wsfed_endpoint
# Convert WS-Fed endpoint to SAML endpoint (Azure AD pattern)
wsfed_url = wsfed_endpoint.text
config['idp_sso_service_url'] = wsfed_url.gsub('/wsfed', '/saml2')
end
end
# Extract SLO Service URL
slo_service = doc.at_xpath('//IDPSSODescriptor/SingleLogoutService[@Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"]')
slo_service ||= doc.at_xpath('//IDPSSODescriptor/SingleLogoutService[@Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"]')
config['idp_slo_service_url'] = slo_service['Location'] if slo_service
# Extract certificate and calculate fingerprint (try multiple locations)
cert_element = doc.at_xpath('//IDPSSODescriptor/KeyDescriptor[@use="signing"]/KeyInfo/X509Data/X509Certificate')
cert_element ||= doc.at_xpath('//IDPSSODescriptor/KeyDescriptor/KeyInfo/X509Data/X509Certificate')
cert_element ||= doc.at_xpath('//KeyDescriptor[@use="signing"]/KeyInfo/X509Data/X509Certificate')
cert_element ||= doc.at_xpath('//KeyDescriptor/KeyInfo/X509Data/X509Certificate')
cert_element ||= doc.at_xpath('//X509Certificate')
if cert_element
cert_data = cert_element.text.gsub(/\s+/, '')
cert_der = Base64.decode64(cert_data)
fingerprint = Digest::SHA1.hexdigest(cert_der).upcase.scan(/../).join(':')
config['idp_cert_fingerprint'] = fingerprint
end
# Extract Name ID formats
name_id_format = doc.at_xpath('//IDPSSODescriptor/NameIDFormat')
config['name_identifier_format'] = name_id_format.text if name_id_format
# Set default name identifier format if not found
config['name_identifier_format'] ||= 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
# Validation
if config['idp_sso_service_url'].blank?
raise 'No SSO service URL found in metadata. Please ensure this is a valid SAML 2.0 or Azure AD metadata file.'
end
if config['idp_cert_fingerprint'].blank?
raise 'No certificate found in metadata. Please ensure the metadata contains a valid X.509 certificate.'
end
config
rescue Nokogiri::XML::SyntaxError => e
raise "Invalid XML metadata: #{e.message}"
end
end