mirror of https://github.com/docusealco/docuseal
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.
133 lines
5.3 KiB
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
|