Files
chatwoot/test_saml_devise_flow.rb
T
2025-08-27 18:40:13 +05:30

270 lines
8.5 KiB
Ruby

#!/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
<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="#{response_id}"
Version="2.0"
IssueInstant="#{issue_instant}"
Destination="http://localhost:3000/omniauth/saml/callback">
<saml:Issuer>#{saml_settings.sso_url}</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion ID="#{assertion_id}" Version="2.0" IssueInstant="#{issue_instant}">
<saml:Issuer>#{saml_settings.sso_url}</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">#{user.email}</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="#{(Time.now.utc + 300).iso8601}"
Recipient="http://localhost:3000/omniauth/saml/callback"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:AuthnStatement AuthnInstant="#{issue_instant}">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="email">
<saml:AttributeValue>#{user.email}</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="name">
<saml:AttributeValue>#{user.name}</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
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"