123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- require 'keycloak/admin'
- RSpec.describe 'SAML Authentication', authenticated_as: false, integration: true, integration_standalone: :saml, required_envs: %w[KEYCLOAK_BASE_URL KEYCLOAK_ADMIN_USER KEYCLOAK_ADMIN_PASSWORD], type: :system do
- let(:zammad_base_url) { "#{Capybara.app_host}:#{Capybara.current_session.server.port}" }
- let(:zammad_saml_metadata) { "#{zammad_base_url}/auth/saml/metadata" }
- let(:saml_base_url) { ENV['KEYCLOAK_BASE_URL'] }
- let(:saml_client_json) { Rails.root.join('test/data/saml/zammad-client.json').read.gsub('#ZAMMAD_BASE_URL', zammad_base_url) }
- let(:saml_realm_zammad_descriptor) { "#{saml_base_url}/realms/zammad/protocol/saml/descriptor" }
- let(:saml_realm_zammad_accounts) { "#{saml_base_url}/realms/zammad/account" }
- # Only before(:each) can access let() variables.
- before do
- saml_configure_keycloak(zammad_saml_metadata:, saml_client_json:)
- end
- # Shared_examples does not work.
- def login_saml(app: 'desktop')
- case app
- when 'desktop'
- visit '/#login'
- find('.auth-provider--saml').click
- when 'mobile'
- visit '/login', app: :mobile
- find('.icon-saml').click
- end
- saml_login_keycloak
- check_logged_in(app: app)
- end
- def check_logged_in(app: 'desktop')
- find_by_id('app')
- case app
- when 'desktop'
- expect(page).to have_current_route('ticket/view/my_tickets')
- when 'mobile'
- # FIXME: Workaround because the redirect to the mobile app is not working due to a not set HTTP Referer in Capybara.
- visit '/', app: :mobile
- expect(page).to have_text('Home')
- end
- end
- def logout_saml
- await_empty_ajax_queue
- logout
- expect_current_route 'login'
- find_by_id('app')
- end
- # TODO: Should be replaced with tests for the new desktop-view (or the test in general should be removed outside of selenium).
- describe 'SP login and SP logout' do
- before do
- saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, security:)
- end
- let(:security) { nil }
- it 'is successful' do
- login_saml
- visit saml_realm_zammad_accounts
- expect(page).to have_text('John Doe')
- logout_saml
- visit saml_realm_zammad_accounts
- expect(page).to have_text('Sign in')
- end
- context 'with client signature required and encrypted assertions enabled' do
- let(:security) do
- # generate a new private key and certificate
- key = OpenSSL::PKey::RSA.new(2048)
- cert = OpenSSL::X509::Certificate.new
- cert.subject = OpenSSL::X509::Name.parse('/CN=Zammad SAML Client')
- cert.issuer = cert.subject
- cert.not_before = Time.zone.now
- cert.not_after = (cert.not_before + (1 * 365 * 24 * 60 * 60)) # 1 year validity
- cert.public_key = key.public_key
- cert.serial = 0x0
- cert.version = 2
- ef = OpenSSL::X509::ExtensionFactory.new
- ef.subject_certificate = cert
- ef.issuer_certificate = cert
- cert.add_extension(ef.create_extension('keyUsage', 'digitalSignature, keyEncipherment', true))
- cert.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash', false))
- cert.add_extension(ef.create_extension('basicConstraints', 'CA:FALSE', false))
- cert.sign(key, OpenSSL::Digest.new('SHA256'))
- pem = cert.to_pem
- pem.gsub!('-----BEGIN CERTIFICATE-----', '')
- pem.gsub!('-----END CERTIFICATE-----', '')
- pem.delete!("\n").strip!
- cert = pem
- pem = key.to_pem
- pem.gsub!('-----BEGIN RSA PRIVATE KEY-----', '') # gitleaks:allow
- pem.gsub!('-----END RSA PRIVATE KEY-----', '') # gitleaks:allow
- pem.delete!("\n").strip!
- key = pem
- {
- cert:,
- key:
- }
- end
- let(:saml_client_json) do
- client = Rails.root.join('test/data/saml/zammad-client-secure.json').read
- client.gsub!('#KEYCLOAK_ZAMMAD_BASE_URL', zammad_base_url)
- client.gsub!('#KEYCLOAK_ZAMMAD_CERTIFICATE', security[:cert])
- client
- end
- it 'is successful' do
- login_saml
- visit saml_realm_zammad_accounts
- expect(page).to have_text('John Doe')
- logout_saml
- visit saml_realm_zammad_accounts
- expect(page).to have_text('Sign in')
- end
- end
- end
- describe 'SP login and IDP logout' do
- before do
- saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
- end
- it 'is successful' do
- login_saml
- visit saml_realm_zammad_accounts
- click_on 'John Doe'
- find('a', text: 'Sign out').click
- visit '/'
- expect(page).to have_current_route('login')
- find_by_id('app')
- end
- end
- describe "use custom user attribute 'uid' as uid_attribute" do
- before do
- saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, uid_attribute: 'uid')
- end
- it 'is successful' do
- login_saml
- user = User.find_by(email: 'john.doe@saml.example.com')
- expect(user.login).to eq('5f8179df-db5e-415c-8090-6cc3634d86b6')
- logout_saml
- end
- end
- describe 'use unspecified (IDP provided) name identifier' do
- before do
- saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
- end
- it 'is successful' do
- login_saml
- user = User.find_by(email: 'john.doe@saml.example.com')
- expect(user.login).to eq('john.doe')
- logout_saml
- end
- end
- describe 'SAML logout without IDP SLO service URL' do
- before do
- saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, idp_slo_service_url: false)
- end
- it 'is successful' do
- login_saml
- user = User.find_by(email: 'john.doe@saml.example.com')
- expect(user.login).to eq('john.doe@saml.example.com')
- logout_saml
- visit saml_realm_zammad_accounts
- expect(page).to have_text('John Doe')
- end
- end
- describe 'Mobile View', app: :mobile do
- before do
- skip 'Skip mobile tests enforced.' if ENV['SKIP_MOBILE_TESTS']
- end
- context 'when login is tested' do
- before do
- saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
- end
- it 'is successful' do
- login_saml(app: 'mobile')
- visit saml_realm_zammad_accounts
- click_on 'Actions'
- expect(page).to have_text('Sign out')
- end
- end
- context 'when logout is tested' do
- before do
- saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
- end
- it 'is successful' do
- login_saml(app: 'mobile')
- visit '/account', app: :mobile
- click_on('Sign out')
- wait.until do
- expect(page).to have_button('Sign in')
- end
- visit saml_realm_zammad_accounts
- expect(page).to have_text('Sign in')
- end
- end
- context 'when saml user already exists with agent role' do
- before do
- Setting.set('auth_third_party_auto_link_at_inital_login', true)
- create(:agent, email: 'john.doe@saml.example.com', login: 'john.doe', firstname: 'John', lastname: 'Doe')
- saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
- end
- it 'is successful' do
- login_saml(app: 'mobile')
- visit saml_realm_zammad_accounts
- click_on 'Actions'
- expect(page).to have_text('Sign out')
- end
- end
- context 'when logout is tested without IDP SLO service URL' do
- before do
- saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, idp_slo_service_url: false)
- end
- it 'is successful' do
- login_saml(app: 'mobile')
- visit '/account', app: :mobile
- click_on('Sign out')
- wait.until do
- expect(page).to have_button('Sign in')
- end
- visit saml_realm_zammad_accounts
- click_on 'Actions'
- expect(page).to have_text('Sign out')
- end
- end
- end
- end
|