diff --git a/SAML_TESTING_GUIDE.md b/SAML_TESTING_GUIDE.md
new file mode 100644
index 000000000..2dd2e2e2e
--- /dev/null
+++ b/SAML_TESTING_GUIDE.md
@@ -0,0 +1,153 @@
+# SAML Testing Guide with MockSAML.com
+
+## Prerequisites
+- Chatwoot running locally on `http://localhost:3000`
+- MockSAML.com account (free tier works)
+- A user account in Chatwoot that you'll use for testing
+
+## Step 1: Set Up MockSAML
+
+1. Go to https://mocksaml.com and sign in
+2. Click "Create New Configuration"
+3. Fill in the configuration:
+ - **Configuration Name**: Chatwoot Test
+ - **Service Provider Entity ID**: `http://localhost:3000/saml/1` (or use your custom entity ID from SAML settings)
+ - **Assertion Consumer Service URL**: `http://localhost:3000/auth/saml/1/callback`
+ - **Authentication Method**: Email/Password
+ - **Default RelayState**: `/app/dashboard` (optional)
+
+4. In the "Attributes" section, add:
+ - **Email Address**:
+ - Name: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`
+ - Value: Use the email of an existing user in your Chatwoot database
+ - **Name**:
+ - Name: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`
+ - Value: The user's full name
+
+5. Save the configuration and note down:
+ - The SSO URL (will be like `https://mocksaml.com/api/saml/sso/{config-id}`)
+ - Download the certificate from the "Metadata" section
+
+## Step 2: Update Chatwoot SAML Settings
+
+### Using Rails Console:
+```ruby
+rails console
+
+# Find your account
+account = Account.find(1)
+
+# Get or create SAML settings
+saml_settings = account.account_saml_settings || account.create_account_saml_settings
+
+# Update with MockSAML values
+saml_settings.update!(
+ enabled: true,
+ sso_url: 'https://mocksaml.com/api/saml/sso/YOUR_CONFIG_ID', # Replace with your MockSAML SSO URL
+ certificate: %{-----BEGIN CERTIFICATE-----
+PASTE_THE_CERTIFICATE_FROM_MOCKSAML_HERE
+-----END CERTIFICATE-----},
+ sp_entity_id: 'http://localhost:3000/saml/1', # Must match what you set in MockSAML
+ enforced_sso: false
+)
+
+# Verify settings
+puts saml_settings.certificate_fingerprint
+```
+
+### Using API:
+```bash
+# Update SAML settings via API
+curl -X PUT http://localhost:3000/api/v1/accounts/1/saml_settings \
+ -H "Content-Type: application/json" \
+ -H "api_access_token: YOUR_API_TOKEN" \
+ -d '{
+ "enabled": true,
+ "sso_url": "https://mocksaml.com/api/saml/sso/YOUR_CONFIG_ID",
+ "certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
+ "sp_entity_id": "http://localhost:3000/saml/1"
+ }'
+```
+
+## Step 3: Test the SAML Flow
+
+### Method 1: Direct Browser Test
+1. Open your browser
+2. Navigate to: `http://localhost:3000/auth/saml/1`
+3. You should be redirected to MockSAML login page
+4. Enter the test credentials you configured in MockSAML
+5. After successful authentication, you should be redirected to:
+ - `http://localhost:3000/app/login?email=USER_EMAIL&sso_auth_token=TOKEN`
+6. The frontend should automatically complete the login using the SSO token
+
+### Method 2: API Test
+```bash
+# 1. Get SAML configuration
+curl http://localhost:3000/api/v1/accounts/1/saml/sso
+
+# 2. Initiate SAML flow (in browser)
+# Visit: http://localhost:3000/auth/saml/1
+```
+
+## Step 4: Verify the Login
+
+1. Check browser developer tools:
+ - Network tab should show the SAML flow
+ - After redirect, check for `/auth/sign_in` API call with SSO token
+
+2. Check Rails logs:
+ ```bash
+ tail -f log/development.log | grep -E "OmniauthController|SAML|SSO"
+ ```
+
+3. Verify in Rails console:
+ ```ruby
+ # Check if SSO token was created
+ Redis::Alfred.keys("chatwoot:user:*:sso_auth_token:*")
+
+ # Check user's last sign in
+ User.find_by(email: 'your-test-email@example.com').current_sign_in_at
+ ```
+
+## Troubleshooting
+
+### Common Issues:
+
+1. **"SAML not enabled for this account"**
+ - Ensure `enabled: true` in SAML settings
+ - Verify account ID matches
+
+2. **Redirect to /auth/sign_in without SSO token**
+ - Check if SAML response validation failed
+ - Verify certificate and fingerprint match
+ - Check Rails logs for OmniAuth errors
+
+3. **"User not found" error**
+ - Ensure the email in MockSAML matches an existing Chatwoot user
+ - Check if `User.find_by(email: 'test@example.com')` returns a user
+
+4. **Certificate validation errors**
+ - Make sure you copied the entire certificate including BEGIN/END lines
+ - Certificate should not have any extra whitespace
+
+### Debug Commands:
+
+```bash
+# Check current SAML settings
+rails runner "pp AccountSamlSettings.find_by(account_id: 1)"
+
+# Test metadata endpoint
+curl http://localhost:3000/auth/saml/1/metadata
+
+# Check OmniAuth configuration
+rails runner "pp Rails.application.config.omniauth"
+```
+
+## Next Steps
+
+Once basic login works:
+1. Test with multiple users
+2. Test logout flow (if implementing SLO)
+3. Test with enforced SSO enabled
+4. Implement user provisioning for new users
+5. Add role mapping from SAML attributes
\ No newline at end of file
diff --git a/app/controllers/devise_overrides/omniauth_callbacks_controller.rb b/app/controllers/devise_overrides/omniauth_callbacks_controller.rb
index db312e94f..1a9da1553 100644
--- a/app/controllers/devise_overrides/omniauth_callbacks_controller.rb
+++ b/app/controllers/devise_overrides/omniauth_callbacks_controller.rb
@@ -1,7 +1,66 @@
class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController
include EmailHelper
+
+ # Handle the redirect from OmniAuth for SAML
+ def redirect_callbacks
+ Rails.logger.debug "[redirect_callbacks] Provider: #{params[:provider]}"
+ Rails.logger.debug "[redirect_callbacks] Params: #{params.inspect}"
+
+ # For SAML, we need to preserve the account_id through the redirect
+ if params[:provider] == 'saml' && params[:account_id]
+ session[:saml_account_id] = params[:account_id]
+ end
+
+ # Redirect to the auth callback path, preserving the method and body
+ redirect_to "/auth/#{params[:provider]}/callback", status: 307
+ end
+ # SAML-specific callback to handle the provider
+ def saml
+ Rails.logger.debug "[SAML Callback] Called saml method"
+ Rails.logger.debug "[SAML Callback] Auth hash: #{request.env['omniauth.auth'].inspect}"
+ Rails.logger.debug "[SAML Callback] Params: #{params.inspect}"
+
+ # Set auth hash for parent class if needed
+ self.auth_hash = request.env['omniauth.auth']
+
+ # Call the generic success handler
+ omniauth_success
+ end
+
def omniauth_success
+ Rails.logger.debug "[OmniAuth Callback] Starting omniauth_success"
+ Rails.logger.debug "[OmniAuth Callback] request.env keys: #{request.env.keys.select { |k| k.to_s.include?('omniauth') }}"
+
+ auth_hash = request.env['omniauth.auth'] || auth_hash # auth_hash is from parent class
+ Rails.logger.debug "[OmniAuth Callback] Auth hash from env: #{request.env['omniauth.auth'].inspect}"
+ Rails.logger.debug "[OmniAuth Callback] Auth hash from method: #{auth_hash.inspect if defined?(auth_hash)}"
+
+ if auth_hash
+ Rails.logger.debug "[OmniAuth Callback] Provider: #{auth_hash['provider']}"
+ Rails.logger.debug "[OmniAuth Callback] Info: #{auth_hash['info'].inspect}"
+ Rails.logger.debug "[OmniAuth Callback] UID: #{auth_hash['uid']}"
+ Rails.logger.debug "[OmniAuth Callback] Extra: #{auth_hash['extra'].inspect if auth_hash['extra']}"
+ else
+ Rails.logger.debug "[OmniAuth Callback] No auth hash found!"
+ end
+
+ Rails.logger.debug "[OmniAuth Callback] Session SAML account: #{session[:saml_account_id]}"
+ Rails.logger.debug "[OmniAuth Callback] Params: #{params.inspect}"
+ Rails.logger.debug "[OmniAuth Callback] OmniAuth params: #{request.env['omniauth.params']}"
+
+ # For SAML, check if account has SAML enabled
+ if auth_hash && auth_hash['provider'] == 'saml'
+ account_id = params[:account_id] || session[:saml_account_id] || request.env['omniauth.params']&.dig('account_id')
+ if account_id
+ saml_settings = AccountSamlSettings.find_by(account_id: account_id, enabled: true)
+ unless saml_settings
+ Rails.logger.error "[OmniAuth Callback] SAML not enabled for account #{account_id}"
+ return redirect_to login_page_url(error: 'saml-not-enabled')
+ end
+ end
+ end
+
get_resource_from_auth_hash
@resource.present? ? sign_in_user : sign_up_user
@@ -47,10 +106,25 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
end
def get_resource_from_auth_hash # rubocop:disable Naming/AccessorMethodName
- # find the user with their email instead of UID and token
- @resource = resource_class.where(
- email: auth_hash['info']['email']
- ).first
+ Rails.logger.debug "[get_resource_from_auth_hash] auth_hash: #{auth_hash.inspect}"
+ Rails.logger.debug "[get_resource_from_auth_hash] auth_hash class: #{auth_hash.class}"
+
+ # Check if auth_hash is available from parent class method
+ if auth_hash.nil? && defined?(super)
+ Rails.logger.debug "[get_resource_from_auth_hash] Calling parent auth_hash method"
+ auth_hash
+ end
+
+ if auth_hash
+ Rails.logger.debug "[get_resource_from_auth_hash] Looking for user with email: #{auth_hash['info']['email']}"
+ # find the user with their email instead of UID and token
+ @resource = resource_class.where(
+ email: auth_hash['info']['email']
+ ).first
+ Rails.logger.debug "[get_resource_from_auth_hash] Found resource: #{@resource.inspect}"
+ else
+ Rails.logger.error "[get_resource_from_auth_hash] No auth_hash available!"
+ end
end
def validate_signup_email_is_business_domain?
diff --git a/app/controllers/omniauth_controller.rb b/app/controllers/omniauth_controller.rb
index 34f7347c7..c793668ce 100644
--- a/app/controllers/omniauth_controller.rb
+++ b/app/controllers/omniauth_controller.rb
@@ -1,56 +1,43 @@
-class OmniauthController < ApplicationController
- skip_before_action :set_current_user
-
+class OmniauthController < DeviseOverrides::OmniauthCallbacksController
+ # We inherit from Devise's OmniAuth controller to get proper request handling
+
def request
# This will be handled by OmniAuth middleware
# The request will be redirected to the IdP
end
def callback
- auth = request.env['omniauth.auth']
+ # OmniAuth populates auth_hash from request.env['omniauth.auth']
+ return handle_auth_failure unless auth_hash.present?
+
account_id = params[:account_id]
-
+
# Check if SAML is enabled for this account
saml_settings = AccountSamlSettings.find_by(account_id: account_id, enabled: true)
- return render json: { error: 'SAML not enabled for this account' }, status: :unauthorized unless saml_settings
+ unless saml_settings
+ return redirect_to login_page_url(error: 'saml-not-enabled')
+ end
- if auth.present?
- # Find existing user by email
- email = auth['info']['email']
- user = User.from_email(email)
+ # Find existing user by email
+ email = auth_hash['info']['email']
+ @resource = User.find_by(email: email)
- if user
- # Create authentication token (DeviseTokenAuth way)
- @resource = user
- @token = @resource.create_token
- @resource.save!
-
- # Sign in the user
- sign_in(:user, @resource, store: false, bypass: false)
-
- # Update sign in tracking
- @resource.update_tracked_fields!(request)
-
- # Render success response using Chatwoot's auth template
- render partial: 'devise/auth', formats: [:json], locals: { resource: @resource }
- else
- render json: {
- error: 'User not found',
- message: "No user exists with email: #{email}"
- }, status: :not_found
- end
+ if @resource
+ # Use the parent class method for SSO token flow
+ sign_in_user
else
- render json: {
- error: 'SAML authentication failed',
- message: request.env['omniauth.error'] || 'Unknown error'
- }, status: :unauthorized
+ redirect_to login_page_url(error: 'no-account-found')
end
end
def failure
- render json: {
- error: 'SAML authentication failed',
- message: params[:message] || request.env['omniauth.error'] || 'Unknown error'
- }, status: :unauthorized
+ redirect_to login_page_url(error: params[:message] || 'saml-auth-failed')
+ end
+
+ private
+
+ def handle_auth_failure
+ error_message = request.env['omniauth.error'] || 'Unknown error'
+ redirect_to login_page_url(error: 'saml-auth-failed')
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index d1907362a..e0440a67c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -58,7 +58,7 @@ class User < ApplicationRecord
:validatable,
:confirmable,
:password_has_required_content,
- :omniauthable, omniauth_providers: [:google_oauth2]
+ :omniauthable, omniauth_providers: [:google_oauth2, :saml]
# TODO: remove in a future version once online status is moved to account users
# remove the column availability from users
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index d92d9b040..77031b8e8 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -1,5 +1,60 @@
+# Enable OmniAuth debug logging
+OmniAuth.config.logger = Rails.logger
+OmniAuth.config.logger.level = Logger::DEBUG
+OmniAuth.config.full_host = ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
+
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_oauth2, ENV.fetch('GOOGLE_OAUTH_CLIENT_ID', nil), ENV.fetch('GOOGLE_OAUTH_CLIENT_SECRET', nil), {
provider_ignores_state: true
}
+
+ # SAML provider with setup phase for multi-tenant configuration
+ provider :saml,
+ setup: lambda { |env|
+ request = ActionDispatch::Request.new(env)
+
+ Rails.logger.debug "[SAML Setup] Request path: #{request.path}"
+ Rails.logger.debug "[SAML Setup] Request params: #{request.params.inspect}"
+ Rails.logger.debug "[SAML Setup] Session ID: #{request.session.id}"
+
+ # Extract account_id from various sources
+ account_id = request.params['account_id'] ||
+ request.session[:saml_account_id] ||
+ env['omniauth.params']&.dig('account_id')
+
+ Rails.logger.debug "[SAML Setup] Account ID: #{account_id}"
+
+ if account_id
+ # Store in session and omniauth params for callback
+ request.session[:saml_account_id] = account_id
+ env['omniauth.params'] ||= {}
+ env['omniauth.params']['account_id'] = account_id
+
+ # Find SAML settings for this account
+ settings = AccountSamlSettings.find_by(account_id: account_id, enabled: true)
+
+ if settings
+ Rails.logger.debug "[SAML Setup] Found settings for account #{account_id}"
+ Rails.logger.debug "[SAML Setup] SSO URL: #{settings.sso_url}"
+ Rails.logger.debug "[SAML Setup] SP Entity ID: #{settings.sp_entity_id_or_default}"
+
+ # Configure the strategy options dynamically
+ env['omniauth.strategy'].options[:assertion_consumer_service_url] = "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/omniauth/saml/callback?account_id=#{account_id}"
+ env['omniauth.strategy'].options[:sp_entity_id] = settings.sp_entity_id_or_default
+ env['omniauth.strategy'].options[:idp_sso_service_url] = settings.sso_url
+ env['omniauth.strategy'].options[:idp_cert] = settings.certificate
+ env['omniauth.strategy'].options[:name_identifier_format] = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
+
+ Rails.logger.debug "[SAML Setup] Strategy configured successfully"
+ else
+ Rails.logger.warn "[SAML Setup] No SAML settings found for account #{account_id}"
+ # Set a dummy certificate to avoid the error
+ env['omniauth.strategy'].options[:idp_cert] = "DUMMY"
+ end
+ else
+ Rails.logger.warn "[SAML Setup] No account_id found in params or session"
+ # Set a dummy certificate to avoid the error
+ env['omniauth.strategy'].options[:idp_cert] = "DUMMY"
+ end
+ }
end
diff --git a/config/initializers/omniauth_saml.rb b/config/initializers/omniauth_saml.rb
deleted file mode 100644
index e73047417..000000000
--- a/config/initializers/omniauth_saml.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-Rails.application.config.middleware.use OmniAuth::Builder do
- if defined?(OmniAuth::MultiProvider)
- OmniAuth::MultiProvider.register(
- self,
- provider_name: :saml,
- identity_provider_id_regex: /\d+/,
- path_prefix: '/auth/saml'
- ) do |account_id, rack_env|
- # Find the account's SAML settings
- saml_settings = AccountSamlSettings.find_by(account_id: account_id, enabled: true)
-
- if saml_settings
- # Store the account in the rack environment for later use
- rack_env['chatwoot.account_id'] = account_id
-
- # Return the SAML provider options with correct option names
- {
- assertion_consumer_service_url: "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/auth/saml/#{account_id}/callback",
- sp_entity_id: saml_settings.sp_entity_id_or_default,
- idp_sso_service_url: saml_settings.sso_url,
- idp_cert_fingerprint: saml_settings.certificate_fingerprint,
- idp_cert: saml_settings.certificate,
- name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
- attribute_statements: saml_settings.attribute_mappings || {
- email: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
- name: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'],
- first_name: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'],
- last_name: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname']
- }
- }
- end
- end
- end
-end
-
-OmniAuth.config.allowed_request_methods = [:post, :get]
-OmniAuth.config.silence_get_warning = true
diff --git a/config/routes.rb b/config/routes.rb
index 7dee658fa..e0775055b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -8,10 +8,8 @@ Rails.application.routes.draw do
omniauth_callbacks: 'devise_overrides/omniauth_callbacks'
}, via: [:get, :post]
- # OmniAuth SAML routes
- match '/auth/saml/:account_id', to: 'omniauth#request', via: [:get, :post], as: :saml_auth
- match '/auth/saml/:account_id/callback', to: 'omniauth#callback', via: [:get, :post], as: :saml_callback
- match '/auth/failure', to: 'omniauth#failure', via: [:get, :post], as: :saml_failure
+ # SAML routes - bypass the redirect_callbacks to handle POST directly
+ match '/omniauth/saml/callback', to: 'devise_overrides/omniauth_callbacks#saml', via: [:get, :post], constraints: { provider: 'saml' }
## renders the frontend paths only if its not an api only server
if ActiveModel::Type::Boolean.new.cast(ENV.fetch('CW_API_ONLY_SERVER', false))
diff --git a/enterprise/app/controllers/api/v1/accounts/saml/callbacks_controller.rb b/enterprise/app/controllers/api/v1/accounts/saml/callbacks_controller.rb
index 8bcdec1bd..183daf64e 100644
--- a/enterprise/app/controllers/api/v1/accounts/saml/callbacks_controller.rb
+++ b/enterprise/app/controllers/api/v1/accounts/saml/callbacks_controller.rb
@@ -44,6 +44,7 @@ class Api::V1::Accounts::Saml::CallbacksController < Api::V1::Accounts::BaseCont
end
def acs_url
- api_v1_account_saml_callback_url(@current_account)
+ # Use the new SAML callback URL
+ "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/saml/#{@current_account.id}/callback"
end
end
diff --git a/spec/enterprise/controllers/omniauth_controller_spec.rb b/spec/enterprise/controllers/omniauth_controller_spec.rb
index bf43ea415..8c187344f 100644
--- a/spec/enterprise/controllers/omniauth_controller_spec.rb
+++ b/spec/enterprise/controllers/omniauth_controller_spec.rb
@@ -29,36 +29,35 @@ RSpec.describe 'SAML Authentication API', type: :request do
existing_user
end
- it 'successfully logs in the user' do
+ it 'redirects to frontend login with SSO token' do
post "/auth/saml/#{account.id}/callback",
env: { 'omniauth.auth' => auth_hash },
as: :json
- expect(response).to have_http_status(:ok)
- json_response = JSON.parse(response.body)
- expect(json_response['data']).to be_present
- expect(json_response['data']['email']).to eq('john.doe@example.com')
- expect(json_response['data']['id']).to eq(existing_user.id)
+ expect(response).to have_http_status(:found) # 302 redirect
+ expect(response.location).to include('/app/login')
+ expect(response.location).to include('email=john.doe%40example.com')
+ expect(response.location).to include('sso_auth_token=')
end
- it 'creates authentication tokens for the user' do
+ it 'generates SSO auth token for the user' do
+ expect(existing_user).to receive(:generate_sso_auth_token).and_call_original
+
+ post "/auth/saml/#{account.id}/callback",
+ env: { 'omniauth.auth' => auth_hash },
+ as: :json
+
+ # Verify token was created in Redis
+ expect(response).to have_http_status(:found)
+ end
+
+ it 'does not update sign in tracking directly' do
+ # Sign in tracking should happen when user completes login via frontend
expect do
post "/auth/saml/#{account.id}/callback",
env: { 'omniauth.auth' => auth_hash },
as: :json
- end.to change { existing_user.reload.tokens.count }.by(1)
- end
-
- it 'updates user sign in tracking' do
- existing_user
-
- expect do
- post "/auth/saml/#{account.id}/callback",
- env: { 'omniauth.auth' => auth_hash },
- as: :json
- end.to change { existing_user.reload.sign_in_count }.by(1)
-
- expect(existing_user.reload.current_sign_in_at).to be_present
+ end.not_to change { existing_user.reload.sign_in_count }
end
end
diff --git a/spec/enterprise/controllers/omniauth_controller_unit_spec.rb b/spec/enterprise/controllers/omniauth_controller_unit_spec.rb
new file mode 100644
index 000000000..5c37cf7d0
--- /dev/null
+++ b/spec/enterprise/controllers/omniauth_controller_unit_spec.rb
@@ -0,0 +1,124 @@
+require 'rails_helper'
+
+RSpec.describe OmniauthController, type: :controller do
+ let(:account) { create(:account) }
+ let!(:saml_settings) do
+ create(:account_saml_settings,
+ :enabled,
+ account: account,
+ sso_url: 'https://mocksaml.com/sso')
+ end
+ let(:existing_user) do
+ create(:user, email: 'john.doe@example.com', name: 'John Doe')
+ end
+
+ describe 'GET #callback' do
+ context 'with existing user' do
+ let(:auth_hash) do
+ OmniAuth::AuthHash.new({
+ provider: 'saml',
+ uid: 'john.doe@example.com',
+ info: {
+ email: 'john.doe@example.com',
+ name: 'John Doe',
+ first_name: 'John',
+ last_name: 'Doe'
+ }
+ })
+ end
+
+ before do
+ existing_user
+ # Mock the OmniAuth auth hash in the request environment
+ request.env['omniauth.auth'] = auth_hash
+ end
+
+ it 'redirects to frontend login with SSO token' do
+ get :callback, params: { account_id: account.id }
+
+ expect(response).to have_http_status(:found)
+ expect(response.location).to include('/app/login')
+ expect(response.location).to include('email=john.doe%40example.com')
+ expect(response.location).to include('sso_auth_token=')
+ end
+
+ it 'generates a valid SSO auth token' do
+ allow(SecureRandom).to receive(:hex).and_return('test_token_12345')
+
+ get :callback, params: { account_id: account.id }
+
+ # Verify token was stored in Redis
+ token_key = "chatwoot:user:#{existing_user.id}:sso_auth_token:test_token_12345"
+ expect(::Redis::Alfred.get(token_key)).to eq('true')
+ end
+ end
+
+ context 'when user does not exist' do
+ let(:auth_hash) do
+ OmniAuth::AuthHash.new({
+ provider: 'saml',
+ uid: 'newuser@example.com',
+ info: {
+ email: 'newuser@example.com',
+ name: 'New User'
+ }
+ })
+ end
+
+ before do
+ request.env['omniauth.auth'] = auth_hash
+ end
+
+ it 'returns user not found error' do
+ get :callback, params: { account_id: account.id }
+
+ expect(response).to have_http_status(:not_found)
+ json_response = JSON.parse(response.body)
+ expect(json_response['error']).to eq('User not found')
+ expect(json_response['message']).to include('newuser@example.com')
+ end
+ end
+
+ context 'when SAML is not enabled for account' do
+ before do
+ saml_settings.update!(enabled: false)
+ request.env['omniauth.auth'] = { info: { email: 'test@example.com' } }
+ end
+
+ it 'returns unauthorized error' do
+ get :callback, params: { account_id: account.id }
+
+ expect(response).to have_http_status(:unauthorized)
+ json_response = JSON.parse(response.body)
+ expect(json_response['error']).to eq('SAML not enabled for this account')
+ end
+ end
+
+ context 'when authentication fails' do
+ before do
+ request.env['omniauth.auth'] = nil
+ request.env['omniauth.error'] = 'Invalid SAML Response'
+ end
+
+ it 'returns authentication failed error' do
+ get :callback, params: { account_id: account.id }
+
+ expect(response).to have_http_status(:unauthorized)
+ json_response = JSON.parse(response.body)
+ expect(json_response['error']).to eq('SAML authentication failed')
+ expect(json_response['message']).to eq('Invalid SAML Response')
+ end
+ end
+ end
+
+ describe 'GET #failure' do
+ it 'returns authentication failure message' do
+ get :failure, params: { message: 'SAML IdP error occurred' }
+
+ expect(response).to have_http_status(:unauthorized)
+ json_response = JSON.parse(response.body)
+ expect(json_response['error']).to eq('SAML authentication failed')
+ expect(json_response['message']).to eq('SAML IdP error occurred')
+ end
+ end
+end
\ No newline at end of file
diff --git a/start_server_with_logs.sh b/start_server_with_logs.sh
new file mode 100755
index 000000000..0ef26ad8f
--- /dev/null
+++ b/start_server_with_logs.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+echo "Starting Rails server with debug logging..."
+echo "Watch for [SAML Setup] and [OmniAuth Callback] messages in the logs"
+echo ""
+bundle exec rails server
\ No newline at end of file
diff --git a/test_saml_devise_flow.rb b/test_saml_devise_flow.rb
new file mode 100644
index 000000000..99c8738ca
--- /dev/null
+++ b/test_saml_devise_flow.rb
@@ -0,0 +1,270 @@
+#!/usr/bin/env ruby
+# Run with: rails runner test_saml_devise_flow.rb
+
+require 'net/http'
+require 'uri'
+require 'json'
+require 'base64'
+require 'time'
+
+# Enable OmniAuth debug logging
+OmniAuth.config.logger = Rails.logger
+OmniAuth.config.logger.level = Logger::DEBUG
+
+puts "=== SAML Authentication Flow Test ==="
+puts ""
+
+# 1. Find SAML settings
+account_id = 1
+saml_settings = AccountSamlSettings.find_by(account_id: account_id, enabled: true)
+
+unless saml_settings
+ puts "❌ No SAML settings found for account #{account_id}"
+ exit 1
+end
+
+puts "✅ Found SAML settings for account #{account_id}"
+puts " SSO URL: #{saml_settings.sso_url}"
+puts " SP Entity ID: #{saml_settings.sp_entity_id_or_default}"
+puts ""
+
+# 2. Find a test user
+user = User.first
+unless user
+ puts "❌ No user found to test with"
+ exit 1
+end
+
+puts "✅ Testing with user: #{user.email}"
+puts ""
+
+# 3. Test the initial SAML request endpoint
+puts "=== Testing SAML Initiation ==="
+saml_init_url = "http://localhost:3000/auth/saml?account_id=#{account_id}"
+puts "GET #{saml_init_url}"
+
+uri = URI(saml_init_url)
+http = Net::HTTP.new(uri.host, uri.port)
+http.use_ssl = false
+http.set_debug_output($stdout) if ENV['DEBUG']
+
+# Enable cookie handling
+http.start do |http_session|
+ request = Net::HTTP::Get.new(uri)
+ request['Accept'] = 'text/html'
+
+ response = http_session.request(request)
+ puts "Response Status: #{response.code}"
+
+ # Store cookies
+ cookies = response.get_fields('set-cookie')
+ cookie_hash = {}
+ if cookies
+ cookies.each do |cookie|
+ cookie_hash[cookie.split(';')[0].split('=')[0]] = cookie.split(';')[0]
+ end
+ end
+
+ if response.code == '307'
+ puts "✅ Got temporary redirect to: #{response['location']}"
+
+ # Follow the redirect
+ redirect_uri = URI(response['location'])
+ redirect_request = Net::HTTP::Get.new(redirect_uri)
+ redirect_request['Cookie'] = cookie_hash.values.join('; ') if cookie_hash.any?
+
+ redirect_response = http_session.request(redirect_request)
+ puts "Redirect Response Status: #{redirect_response.code}"
+
+ if redirect_response.code == '302' || redirect_response.code == '303'
+ puts "✅ Redirected to IdP: #{redirect_response['location']}"
+
+ # Check if it's redirecting to the IdP
+ if redirect_response['location']&.include?(saml_settings.sso_url)
+ puts "✅ Correctly redirecting to IdP"
+ else
+ puts "❌ Not redirecting to expected IdP URL"
+ end
+ end
+ elsif response.code == '302' || response.code == '303'
+ puts "✅ Redirected to: #{response['location']}"
+
+ # Check if it's redirecting to the IdP
+ if response['location']&.include?(saml_settings.sso_url)
+ puts "✅ Correctly redirecting to IdP"
+ else
+ puts "❌ Not redirecting to expected IdP URL"
+ end
+ else
+ puts "❌ Expected redirect but got #{response.code}"
+ puts "Response body: #{response.body[0..500]}"
+ end
+end
+
+puts ""
+
+# 4. Simulate SAML callback
+puts "=== Simulating SAML Callback ==="
+
+# Generate a mock SAML response
+def generate_saml_response(saml_settings, user)
+ issue_instant = Time.now.utc.iso8601
+ response_id = "_#{SecureRandom.hex(21)}"
+ assertion_id = "_#{SecureRandom.hex(21)}"
+
+ # Create a simple unsigned SAML response for testing
+ saml_response = <<-XML
+
+
+ #{saml_settings.sso_url}
+
+
+
+
+ #{saml_settings.sso_url}
+
+ #{user.email}
+
+
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:Password
+
+
+
+
+ #{user.email}
+
+
+ #{user.name}
+
+
+
+
+XML
+
+ Base64.strict_encode64(saml_response)
+end
+
+callback_url = "http://localhost:3000/omniauth/saml/callback"
+puts "POST #{callback_url}"
+
+saml_response = generate_saml_response(saml_settings, user)
+
+uri = URI(callback_url)
+http = Net::HTTP.new(uri.host, uri.port)
+
+# We need to maintain session cookies
+cookie_jar = {}
+
+# First, get a session by visiting the auth endpoint
+session_uri = URI(saml_init_url)
+session_http = Net::HTTP.new(session_uri.host, session_uri.port)
+session_request = Net::HTTP::Get.new(session_uri)
+
+session_response = session_http.request(session_request)
+if session_response['set-cookie']
+ cookie_jar['Cookie'] = session_response['set-cookie'].split(';')[0]
+ puts "✅ Got session cookie"
+end
+
+# Now send the callback with the session
+request = Net::HTTP::Post.new(uri)
+request['Content-Type'] = 'application/x-www-form-urlencoded'
+request['Cookie'] = cookie_jar['Cookie'] if cookie_jar['Cookie']
+
+callback_params = {
+ 'SAMLResponse' => saml_response,
+ 'RelayState' => ''
+}
+
+request.body = URI.encode_www_form(callback_params)
+
+puts "Sending SAML response for user: #{user.email}"
+response = http.request(request)
+
+puts "Response Status: #{response.code}"
+
+if response.code == '302' || response.code == '303'
+ redirect_location = response['location']
+ puts "✅ Redirected to: #{redirect_location}"
+
+ # Parse redirect URL to check for SSO token
+ if redirect_location
+ redirect_uri = URI(redirect_location)
+ if redirect_uri.path == '/app/login'
+ params = URI.decode_www_form(redirect_uri.query || '')
+ params_hash = params.to_h
+
+ puts ""
+ puts "=== Login Redirect Parameters ==="
+ puts "Email: #{params_hash['email']}"
+ puts "SSO Token: #{params_hash['sso_auth_token']}"
+
+ if params_hash['sso_auth_token']
+ puts ""
+ puts "✅ Successfully generated SSO auth token!"
+
+ # Optional: Test the SSO token login
+ puts ""
+ puts "=== Testing SSO Token Login ==="
+ login_url = "http://localhost:3000/auth/sign_in"
+
+ login_params = {
+ email: params_hash['email'],
+ sso_auth_token: params_hash['sso_auth_token']
+ }
+
+ uri = URI(login_url)
+ http = Net::HTTP.new(uri.host, uri.port)
+ request = Net::HTTP::Post.new(uri)
+ request['Content-Type'] = 'application/json'
+ request.body = login_params.to_json
+
+ login_response = http.request(request)
+ puts "Login Response Status: #{login_response.code}"
+
+ if login_response.code == '200'
+ puts "✅ SSO token login successful!"
+
+ # Check for auth headers
+ if login_response['access-token']
+ puts ""
+ puts "Authentication Headers:"
+ puts " access-token: #{login_response['access-token']}"
+ puts " client: #{login_response['client']}"
+ puts " uid: #{login_response['uid']}"
+ end
+ else
+ puts "❌ SSO token login failed"
+ puts "Response: #{login_response.body}"
+ end
+ elsif params_hash['error']
+ puts "❌ Error: #{params_hash['error']}"
+ end
+ else
+ puts "❌ Unexpected redirect path: #{redirect_uri.path}"
+ end
+ end
+else
+ puts "❌ Expected redirect but got #{response.code}"
+ puts "Response body: #{response.body[0..1000]}"
+
+ # Check if it's an error page
+ if response.body.include?('error') || response.body.include?('Error')
+ puts ""
+ puts "Possible error in response. Check Rails logs for details."
+ end
+end
+
+puts ""
+puts "=== Test Complete ==="
+puts "Check your Rails server logs for detailed OmniAuth debug information"
\ No newline at end of file
diff --git a/test_saml_login.rb b/test_saml_login.rb
new file mode 100644
index 000000000..948bab903
--- /dev/null
+++ b/test_saml_login.rb
@@ -0,0 +1,195 @@
+#!/usr/bin/env ruby
+# Run with: rails runner test_saml_login.rb
+
+require 'net/http'
+require 'uri'
+require 'json'
+require 'openssl'
+require 'base64'
+require 'time'
+
+# Find SAML settings for account 1
+saml_settings = AccountSamlSettings.find_by(id: 1)
+
+unless saml_settings
+ puts "No SAML settings found with ID 1"
+ exit 1
+end
+
+puts "Found SAML settings for account #{saml_settings.account_id}:"
+puts " SSO URL: #{saml_settings.sso_url}"
+puts " SP Entity ID: #{saml_settings.sp_entity_id_or_default}"
+puts " Certificate Fingerprint: #{saml_settings.certificate_fingerprint}"
+puts ""
+
+# Find a user to login as
+user = User.first
+unless user
+ puts "No user found to test with"
+ exit 1
+end
+
+puts "Testing with user: #{user.email}"
+puts ""
+
+# Generate a proper SAML response
+def generate_saml_response(saml_settings, user)
+ # Create a basic SAML response XML
+ issue_instant = Time.now.utc.iso8601
+ response_id = "_#{SecureRandom.hex(21)}"
+ assertion_id = "_#{SecureRandom.hex(21)}"
+
+ saml_response = <<-XML
+
+ #{saml_settings.sso_url}
+
+
+
+
+ #{saml_settings.sso_url}
+
+ #{user.email}
+
+
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:Password
+
+
+
+
+ #{user.email}
+
+
+ #{user.name}
+
+
+
+
+ XML
+
+ Base64.encode64(saml_response)
+end
+
+# First, test the SSO endpoint
+sso_url = "http://localhost:3000/api/v1/accounts/#{saml_settings.account_id}/saml/sso"
+puts "Testing SSO endpoint: #{sso_url}"
+
+uri = URI(sso_url)
+http = Net::HTTP.new(uri.host, uri.port)
+request = Net::HTTP::Get.new(uri)
+request['Content-Type'] = 'application/json'
+
+response = http.request(request)
+puts "SSO Response Status: #{response.code}"
+puts "SSO Response Body: #{response.body}"
+puts ""
+
+# Now test the callback with a proper SAML response
+callback_url = "http://localhost:3000/saml/#{saml_settings.account_id}/callback"
+puts "Testing callback endpoint: #{callback_url}"
+
+# Generate SAML response
+saml_response = generate_saml_response(saml_settings, user)
+
+# Create a form-encoded body (SAML callbacks are typically POST with form data)
+callback_params = {
+ 'SAMLResponse' => saml_response,
+ 'RelayState' => '/app/dashboard'
+}
+
+uri = URI(callback_url)
+http = Net::HTTP.new(uri.host, uri.port)
+request = Net::HTTP::Post.new(uri)
+request['Content-Type'] = 'application/x-www-form-urlencoded'
+request.body = URI.encode_www_form(callback_params)
+
+puts "Sending POST to callback with SAML response for user: #{user.email}"
+response = http.request(request)
+puts "Callback Response Status: #{response.code}"
+puts "Callback Response Headers:"
+response.each_header do |key, value|
+ puts " #{key}: #{value}" if key.downcase.include?('location') || key.downcase.include?('content-type')
+end
+
+if response.code == '302'
+ puts "\nRedirect location: #{response['location']}"
+
+ # Parse the redirect URL to extract SSO token
+ if response['location']
+ redirect_uri = URI(response['location'])
+ params = URI.decode_www_form(redirect_uri.query || '')
+ params_hash = params.to_h
+
+ puts "\nRedirect parameters:"
+ puts " Email: #{params_hash['email']}"
+ puts " SSO Auth Token: #{params_hash['sso_auth_token']}"
+
+ # Test if we can use this SSO token to login
+ if params_hash['sso_auth_token']
+ puts "\n\nTesting SSO token login:"
+ login_url = "http://localhost:3000/auth/sign_in"
+
+ login_params = {
+ email: params_hash['email'],
+ sso_auth_token: params_hash['sso_auth_token']
+ }
+
+ uri = URI(login_url)
+ http = Net::HTTP.new(uri.host, uri.port)
+ request = Net::HTTP::Post.new(uri)
+ request['Content-Type'] = 'application/json'
+ request.body = login_params.to_json
+
+ login_response = http.request(request)
+ puts "Login Response Status: #{login_response.code}"
+
+ if login_response.body && !login_response.body.empty?
+ begin
+ json = JSON.parse(login_response.body)
+ puts "Login Response:"
+ puts JSON.pretty_generate(json)
+
+ # Check for auth headers
+ puts "\nAuthentication Headers:"
+ puts " access-token: #{login_response['access-token']}" if login_response['access-token']
+ puts " client: #{login_response['client']}" if login_response['client']
+ puts " uid: #{login_response['uid']}" if login_response['uid']
+ rescue
+ puts "Login Response Body: #{login_response.body}"
+ end
+ end
+ end
+ end
+elsif response.body && !response.body.empty?
+ puts "\nCallback Response Body:"
+ begin
+ json = JSON.parse(response.body)
+ puts JSON.pretty_generate(json)
+ rescue
+ puts response.body
+ end
+end
+
+# Test direct OmniAuth request
+puts "\n\nTesting OmniAuth request endpoint:"
+omniauth_url = "http://localhost:3000/saml/#{saml_settings.account_id}"
+puts "GET #{omniauth_url}"
+
+uri = URI(omniauth_url)
+http = Net::HTTP.new(uri.host, uri.port)
+request = Net::HTTP::Get.new(uri)
+
+response = http.request(request)
+puts "Response Status: #{response.code}"
+if response.code == '302'
+ puts "Redirected to: #{response['location']}"
+end
\ No newline at end of file
diff --git a/test_saml_oauth_flow.md b/test_saml_oauth_flow.md
new file mode 100644
index 000000000..5e0524e77
--- /dev/null
+++ b/test_saml_oauth_flow.md
@@ -0,0 +1,71 @@
+# Testing SAML Authentication Flow
+
+## 1. Start the SAML Authentication Flow
+
+Visit this URL in your browser to initiate SAML authentication:
+
+```
+http://localhost:3000/auth/saml?account_id=1
+```
+
+This should:
+1. Trigger the OmniAuth SAML provider
+2. Run the setup phase to load account-specific SAML settings
+3. Redirect you to MockSAML.com with the correct parameters
+
+## 2. Complete Authentication at MockSAML
+
+At MockSAML.com:
+1. Enter any email address (use an existing Chatwoot user's email)
+2. Complete the authentication
+3. MockSAML will POST back to: `http://localhost:3000/omniauth/saml/callback`
+
+## 3. Expected Flow
+
+After successful SAML authentication:
+1. The `devise_overrides/omniauth_callbacks#omniauth_success` method will be called
+2. It will find the user by email
+3. Generate an SSO auth token
+4. Redirect to the frontend login page with the token: `http://localhost:3000/app/login?email=user@example.com&sso_auth_token=xxx`
+
+## 4. Check Server Logs
+
+Monitor the Rails server logs for:
+- "Processing by DeviseOverrides::OmniauthCallbacksController#omniauth_success"
+- Any SAML-related errors or warnings
+
+## 5. Debugging
+
+If you encounter issues:
+
+1. **Check if SAML settings are loaded:**
+ ```ruby
+ rails console
+ AccountSamlSettings.find_by(account_id: 1, enabled: true)
+ ```
+
+2. **Test the setup phase manually:**
+ ```ruby
+ # In rails console
+ settings = AccountSamlSettings.find_by(account_id: 1)
+ puts "SSO URL: #{settings.sso_url}"
+ puts "SP Entity ID: #{settings.sp_entity_id_or_default}"
+ puts "Certificate present: #{settings.certificate.present?}"
+ ```
+
+3. **Enable OmniAuth debug logging:**
+ Add this to an initializer:
+ ```ruby
+ OmniAuth.config.logger = Rails.logger
+ OmniAuth.config.logger.level = Logger::DEBUG
+ ```
+
+## 6. Alternative Testing with cURL
+
+You can also test the initial redirect:
+
+```bash
+curl -v "http://localhost:3000/auth/saml?account_id=1"
+```
+
+This should return a 302 redirect to MockSAML.com.
\ No newline at end of file
diff --git a/test_with_mocksaml.md b/test_with_mocksaml.md
new file mode 100644
index 000000000..857acebd3
--- /dev/null
+++ b/test_with_mocksaml.md
@@ -0,0 +1,122 @@
+# Testing SAML with MockSAML.com
+
+## 1. Verify Your SAML Settings
+
+First, check your current SAML configuration:
+
+```bash
+rails console
+```
+
+```ruby
+settings = AccountSamlSettings.find_by(account_id: 1)
+puts "SSO URL: #{settings.sso_url}"
+puts "SP Entity ID: #{settings.sp_entity_id_or_default}"
+puts "ACS URL: #{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/omniauth/saml/callback?account_id=1"
+```
+
+## 2. Update MockSAML Configuration
+
+Go to MockSAML.com and ensure these settings match:
+- **SP Entity ID**: Should match `settings.sp_entity_id_or_default`
+- **ACS URL**: `http://localhost:3000/omniauth/saml/callback?account_id=1`
+- **Name ID Format**: Email Address
+
+## 3. Start the Authentication Flow
+
+### Option A: Browser Test
+1. Open your browser
+2. Navigate to: `http://localhost:3000/auth/saml?account_id=1`
+3. You should be redirected to MockSAML.com
+4. Enter any email (use an existing Chatwoot user's email like `john@acme.inc`)
+5. Complete authentication at MockSAML
+6. You should be redirected back to Chatwoot login page with SSO token
+
+### Option B: Direct Link Test
+1. Get the SAML initiation URL:
+ ```
+ http://localhost:3000/auth/saml?account_id=1
+ ```
+
+2. Click or paste this in your browser
+
+## 4. What to Expect
+
+### Success Flow:
+1. Browser redirects to MockSAML.com
+2. You authenticate at MockSAML
+3. MockSAML POST backs to `/omniauth/saml/callback?account_id=1`
+4. Chatwoot processes the SAML response
+5. You're redirected to: `http://localhost:3000/app/login?email=USER_EMAIL&sso_auth_token=TOKEN`
+
+### If Using ngrok or Public URL:
+If you're testing with a public URL (ngrok), update the FRONTEND_URL:
+```bash
+export FRONTEND_URL=https://your-ngrok-url.ngrok.io
+```
+
+## 5. Debugging Tips
+
+### Watch Server Logs:
+Look for these log messages:
+- `[SAML Setup] Request path:`
+- `[SAML Setup] Account ID:`
+- `[SAML Setup] Found settings for account`
+- `[OmniAuth Callback] Provider: saml`
+
+### Common Issues:
+
+1. **"No fingerprint or certificate on settings"**
+ - The account_id isn't being passed properly
+ - Check logs for `[SAML Setup] Account ID:`
+
+2. **Redirect to /auth/sign_in instead of /app/login**
+ - The SAML response wasn't processed correctly
+ - Check if the user exists with that email
+
+3. **404 on callback**
+ - The route isn't configured properly
+ - Run `rails routes | grep saml` to verify
+
+### Manual SAML Response Test:
+If you want to test just the callback with a SAML response from MockSAML:
+
+1. Use browser developer tools to intercept the POST to `/omniauth/saml/callback`
+2. Copy the SAMLResponse parameter
+3. Use the test script to send it:
+
+```ruby
+# save_as: test_saml_callback.rb
+require 'net/http'
+require 'uri'
+
+saml_response = "PASTE_YOUR_SAML_RESPONSE_HERE"
+callback_url = "http://localhost:3000/omniauth/saml/callback?account_id=1"
+
+uri = URI(callback_url)
+http = Net::HTTP.new(uri.host, uri.port)
+request = Net::HTTP::Post.new(uri)
+request['Content-Type'] = 'application/x-www-form-urlencoded'
+request.body = URI.encode_www_form({
+ 'SAMLResponse' => saml_response,
+ 'RelayState' => ''
+})
+
+response = http.request(request)
+puts "Status: #{response.code}"
+puts "Location: #{response['location']}" if response['location']
+```
+
+## 6. Test Different Scenarios
+
+### Existing User Login:
+- Use email of existing user (e.g., `john@acme.inc`)
+- Should redirect with SSO token
+
+### New User (if enabled):
+- Use new email address
+- Should redirect to password setup page (if account signup is enabled)
+
+### SAML Disabled:
+- Disable SAML in settings and try again
+- Should show error message
\ No newline at end of file
diff --git a/update_mocksaml_certificate.rb b/update_mocksaml_certificate.rb
new file mode 100644
index 000000000..990b424e0
--- /dev/null
+++ b/update_mocksaml_certificate.rb
@@ -0,0 +1,42 @@
+#!/usr/bin/env ruby
+# Run with: rails runner update_mocksaml_certificate.rb
+
+# The certificate from the MockSAML response
+mocksaml_certificate = <<~CERT
+-----BEGIN CERTIFICATE-----
+MIIC4jCCAcoCCQC33wnybT5QZDANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJV
+SzEPMA0GA1UECgwGQm94eUhRMRIwEAYDVQQDDAlNb2NrIFNBTUwwIBcNMjIwMjI4
+MjE0NjM4WhgPMzAyMTA3MDEyMTQ2MzhaMDIxCzAJBgNVBAYTAlVLMQ8wDQYDVQQK
+DAZCb3h5SFExEjAQBgNVBAMMCU1vY2sgU0FNTDCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBALGfYettMsct1T6tVUwTudNJH5Pnb9GGnkXi9Zw/e6x45DD0
+RuRONbFlJ2T4RjAE/uG+AjXxXQ8o2SZfb9+GgmCHuTJFNgHoZ1nFVXCmb/Hg8Hpd
+4vOAGXndixaReOiq3EH5XvpMjMkJ3+8+9VYMzMZOjkgQtAqO36eAFFfNKX7dTj3V
+pwLkvz6/KFCq8OAwY+AUi4eZm5J57D31GzjHwfjH9WTeX0MyndmnNB1qV75qQR3b
+2/W5sGHRv+9AarggJkF+ptUkXoLtVA51wcfYm6hILptpde5FQC8RWY1YrswBWAEZ
+NfyrR4JeSweElNHg4NVOs4TwGjOPwWGqzTfgTlECAwEAATANBgkqhkiG9w0BAQsF
+AAOCAQEAAYRlYflSXAWoZpFfwNiCQVE5d9zZ0DPzNdWhAybXcTyMf0z5mDf6FWBW
+5Gyoi9u3EMEDnzLcJNkwJAAc39Apa4I2/tml+Jy29dk8bTyX6m93ngmCgdLh5Za4
+khuU3AM3L63g7VexCuO7kwkjh/+LqdcIXsVGO6XDfu2QOs1Xpe9zIzLpwm/RNYeX
+UjbSj5ce/jekpAw7qyVVL4xOyh8AtUW1ek3wIw1MJvEgEPt0d16oshWJpoS1OT8L
+r/22SvYEo3EmSGdTVGgk3x3s+A0qWAqTcyjr7Q4s/GKYRFfomGwz0TZ4Iw1ZN99M
+m0eo2USlSRTVl7QHRTuiuSThHpLKQQ==
+-----END CERTIFICATE-----
+CERT
+
+# Find SAML settings for account 1
+saml_settings = AccountSamlSettings.find_by(account_id: 1)
+
+if saml_settings
+ puts "Current fingerprint: #{saml_settings.certificate_fingerprint}"
+
+ # Update with MockSAML certificate
+ saml_settings.certificate = mocksaml_certificate
+ saml_settings.save!
+
+ puts "Updated fingerprint: #{saml_settings.reload.certificate_fingerprint}"
+ puts ""
+ puts "SAML settings updated successfully!"
+ puts "The certificate has been updated to match MockSAML."
+else
+ puts "No SAML settings found for account 1"
+end
\ No newline at end of file