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_login_controller.rb

242 lines
9.4 KiB

# frozen_string_literal: true
class SsoLoginController < ApplicationController
skip_before_action :maybe_redirect_to_setup
skip_before_action :authenticate_user!
skip_authorization_check
# SSO JWT secret key for decoding tokens
SSO_JWT_SECRET = '6a5a4fa4733123c991256f5b0f2221fbe8f7b4c210f74fba621b44c9d5e9f8b6'.freeze
def login
token = params[:token]
template_id = params[:template_id]
unless token.present?
return redirect_to root_path, alert: 'Missing authentication token'
end
begin
# Decode JWT token using the SSO secret key
decoded_token = decode_sso_jwt(token)
email = decoded_token['email']&.downcase
first_name = decoded_token['first_name']
last_name = decoded_token['last_name']
# Check for facility_id/facility_name first (new payload format), then fallback to company_id/company_name
company_id = decoded_token['facility_id'] || decoded_token['company_id'] || decoded_token['account_id'] || decoded_token['organization_id']
company_name = decoded_token['facility_name'] || decoded_token['company_name'] || decoded_token['account_name'] || decoded_token['organization_name']
Rails.logger.info("SSO Login - Email: #{email}, Company ID: #{company_id}, Company Name: #{company_name}")
Rails.logger.info("JWT Payload keys: #{decoded_token.keys.inspect}")
Rails.logger.info("Full JWT Payload: #{decoded_token.inspect}")
unless email.present?
return redirect_to root_path, alert: 'Invalid token: email missing'
end
# Find or create user with company/account
user = find_or_create_user(email, first_name, last_name, company_id, company_name)
if user
# Sign in the user
sign_in(user)
# Redirect to template preview if template_id is present
if template_id.present?
return redirect_to controller: 'templates', action: 'preview', id: template_id
end
# Redirect to dashboard
redirect_to root_path, notice: 'Signed in successfully'
else
redirect_to root_path, alert: 'Unable to sign in'
end
rescue JWT::DecodeError, JWT::ExpiredSignature => e
Rails.logger.error("SSO JWT decode error: #{e.message}")
redirect_to root_path, alert: 'Invalid or expired authentication token'
rescue StandardError => e
Rails.logger.error("SSO login error: #{e.message}")
Rails.logger.error(e.backtrace.join("\n"))
redirect_to root_path, alert: 'An error occurred during sign in'
end
end
private
def decode_sso_jwt(token)
# Decode JWT with the SSO secret key
decoded = JWT.decode(token, SSO_JWT_SECRET, true, { algorithm: 'HS256' })
decoded[0] # Return the payload
end
def find_or_create_user(email, first_name, last_name, company_id = nil, company_name = nil)
# Try to find existing user by email (email is unique globally)
user = User.find_by(email: email)
if user
# User exists - KEEP them in their existing account
# Don't move users between accounts to preserve their data (files, templates, etc.)
account = user.account
Rails.logger.info("User #{email} exists in account #{account.id} (#{account.name}). Keeping in existing account.")
# Log if company_id points to a different account (for debugging)
if company_id.present? || company_name.present?
expected_account = find_or_create_account_by_company(company_id, company_name)
if expected_account && user.account_id != expected_account.id
Rails.logger.warn("User #{email} is in account #{user.account_id} but company_id #{company_id} points to account #{expected_account.id}. User kept in existing account to preserve data.")
end
end
# Don't update name from SSO token for existing users
# This preserves user's manually updated profile information
# Only update if name fields are empty (initial setup)
update_attrs = {}
if user.first_name.blank? && first_name.present?
update_attrs[:first_name] = first_name
end
if user.last_name.blank? && last_name.present?
update_attrs[:last_name] = last_name
end
user.update(update_attrs) if update_attrs.any?
return user
end
# User doesn't exist - create account based on company_id and create new user
account = find_or_create_account_by_company(company_id, company_name)
# Generate a random password for the new user
password = SecureRandom.hex(16)
# Create the new user
user = account.users.build(
email: email,
first_name: first_name || '',
last_name: last_name || '',
password: password,
role: User::ADMIN_ROLE
)
if user.save
user
else
Rails.logger.error("Failed to create user: #{user.errors.full_messages.join(', ')}")
nil
end
end
def find_or_create_account_by_company(company_id = nil, company_name = nil)
Rails.logger.info("Finding or creating account - company_id: #{company_id}, company_name: #{company_name}")
# If company_id is provided, try to find account by ID or UUID
if company_id.present?
# Try to find by ID first (handle both string and integer)
company_id_int = company_id.to_i
account = Account.active.find_by(id: company_id_int) if company_id_int > 0
# If not found by ID, try to find by UUID
account ||= Account.active.find_by(uuid: company_id.to_s) if account.nil?
if account
Rails.logger.info("Found existing account: #{account.id} (#{account.name}) for company_id: #{company_id}")
return account
else
Rails.logger.info("Account not found for company_id: #{company_id}, creating new account")
# Account not found - create new one with company_id reference
account_name = company_name.present? ? company_name : "Company #{company_id}"
account = Account.create!(
name: account_name,
timezone: 'UTC',
locale: 'en-US'
)
initialize_account_configs(account)
Rails.logger.info("Created new account: #{account.id} (#{account.name}) for company_id: #{company_id}")
return account
end
end
# If company_name is provided, try to find by name
if company_name.present?
account = Account.active.find_by(name: company_name)
if account
Rails.logger.info("Found existing account: #{account.id} (#{account.name}) for company_name: #{company_name}")
return account
else
Rails.logger.info("Account not found for company_name: #{company_name}, creating new account")
# Account not found - create new one
account = Account.create!(
name: company_name,
timezone: 'UTC',
locale: 'en-US'
)
initialize_account_configs(account)
Rails.logger.info("Created new account: #{account.id} (#{account.name}) for company_name: #{company_name}")
return account
end
end
# If no company_id or company_name provided
# Check if this is the first user (no accounts exist)
if Account.active.count.zero?
Rails.logger.info("No accounts exist, creating first default account")
account = create_default_account('Default Account')
return account
end
# If no company_id/name provided and accounts exist, create a new account with unique name
# This should not happen in production if company_id is always provided
account_name = "Company #{SecureRandom.hex(4)}"
Rails.logger.warn("No company_id or company_name provided, creating account with random name: #{account_name}")
account = Account.create!(
name: account_name,
timezone: 'UTC',
locale: 'en-US'
)
initialize_account_configs(account)
Rails.logger.info("Created new account: #{account.id} (#{account.name}) without company identifier")
account
end
def create_default_account(name = 'Default Account')
account = Account.create!(
name: name,
timezone: 'UTC',
locale: 'en-US'
)
initialize_account_configs(account)
account
end
def initialize_account_configs(account)
# Create encrypted configs if needed
if EncryptedConfig.table_exists?
app_url = Docuseal.default_url_options[:host] || request.host
app_url = "https://#{app_url}" unless app_url.start_with?('http')
encrypted_configs = [
{ key: EncryptedConfig::APP_URL_KEY, value: app_url }
]
# Only add ESIGN certs if GenerateCertificate is available
begin
encrypted_configs << {
key: EncryptedConfig::ESIGN_CERTS_KEY,
value: GenerateCertificate.call.transform_values(&:to_pem)
}
rescue NameError, StandardError => e
Rails.logger.warn("Could not generate ESIGN certificates: #{e.message}")
end
account.encrypted_configs.create!(encrypted_configs) if encrypted_configs.any?
end
# Create account configs if needed
if AccountConfig.table_exists? && SearchEntry.table_exists?
account.account_configs.create!(key: :fulltext_search, value: true)
end
end
end