saml_spec.rb 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. require 'keycloak/admin'
  4. 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
  5. let(:zammad_base_url) { "#{Capybara.app_host}:#{Capybara.current_session.server.port}" }
  6. let(:zammad_saml_metadata) { "#{zammad_base_url}/auth/saml/metadata" }
  7. let(:saml_base_url) { ENV['KEYCLOAK_BASE_URL'] }
  8. let(:saml_client_json) { Rails.root.join('test/data/saml/zammad-client.json').read.gsub('#ZAMMAD_BASE_URL', zammad_base_url) }
  9. let(:saml_realm_zammad_descriptor) { "#{saml_base_url}/realms/zammad/protocol/saml/descriptor" }
  10. let(:saml_realm_zammad_accounts) { "#{saml_base_url}/realms/zammad/account" }
  11. # Only before(:each) can access let() variables.
  12. before do
  13. saml_configure_keycloak(zammad_saml_metadata:, saml_client_json:)
  14. end
  15. # Shared_examples does not work.
  16. def login_saml(app: 'desktop')
  17. case app
  18. when 'desktop'
  19. visit '/#login'
  20. find('.auth-provider--saml').click
  21. when 'mobile'
  22. visit '/login', app: :mobile
  23. find('.icon-saml').click
  24. end
  25. saml_login_keycloak
  26. check_logged_in(app: app)
  27. end
  28. def check_logged_in(app: 'desktop')
  29. find_by_id('app')
  30. case app
  31. when 'desktop'
  32. expect(page).to have_current_route('ticket/view/my_tickets')
  33. when 'mobile'
  34. # FIXME: Workaround because the redirect to the mobile app is not working due to a not set HTTP Referer in Capybara.
  35. visit '/', app: :mobile
  36. expect(page).to have_text('Home')
  37. end
  38. end
  39. def logout_saml
  40. await_empty_ajax_queue
  41. logout
  42. expect_current_route 'login'
  43. find_by_id('app')
  44. end
  45. def check_mobile_logout_saml
  46. find('.pf-v5-c-masthead__content .pf-v5-c-menu-toggle.pf-m-plain').click
  47. expect(page).to have_text('Sign out')
  48. end
  49. # TODO: Should be replaced with tests for the new desktop-view (or the test in general should be removed outside of selenium).
  50. describe 'SP login and SP logout' do
  51. before do
  52. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, security:)
  53. end
  54. let(:security) { nil }
  55. it 'is successful' do
  56. login_saml
  57. visit saml_realm_zammad_accounts
  58. expect(page).to have_text('John Doe')
  59. logout_saml
  60. visit saml_realm_zammad_accounts
  61. expect(page).to have_text('Sign in')
  62. end
  63. context 'with client signature required and encrypted assertions enabled' do
  64. let(:security) do
  65. # generate a new private key and certificate
  66. key = OpenSSL::PKey::RSA.new(2048)
  67. cert = OpenSSL::X509::Certificate.new
  68. cert.subject = OpenSSL::X509::Name.parse('/CN=Zammad SAML Client')
  69. cert.issuer = cert.subject
  70. cert.not_before = Time.zone.now
  71. cert.not_after = (cert.not_before + (1 * 365 * 24 * 60 * 60)) # 1 year validity
  72. cert.public_key = key.public_key
  73. cert.serial = 0x0
  74. cert.version = 2
  75. ef = OpenSSL::X509::ExtensionFactory.new
  76. ef.subject_certificate = cert
  77. ef.issuer_certificate = cert
  78. cert.add_extension(ef.create_extension('keyUsage', 'digitalSignature, keyEncipherment', true))
  79. cert.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash', false))
  80. cert.add_extension(ef.create_extension('basicConstraints', 'CA:FALSE', false))
  81. cert.sign(key, OpenSSL::Digest.new('SHA256'))
  82. pem = cert.to_pem
  83. pem.gsub!('-----BEGIN CERTIFICATE-----', '')
  84. pem.gsub!('-----END CERTIFICATE-----', '')
  85. pem.delete!("\n").strip!
  86. cert = pem
  87. pem = key.to_pem
  88. pem.gsub!('-----BEGIN RSA PRIVATE KEY-----', '') # gitleaks:allow
  89. pem.gsub!('-----END RSA PRIVATE KEY-----', '') # gitleaks:allow
  90. pem.delete!("\n").strip!
  91. key = pem
  92. {
  93. cert:,
  94. key:
  95. }
  96. end
  97. let(:saml_client_json) do
  98. client = Rails.root.join('test/data/saml/zammad-client-secure.json').read
  99. client.gsub!('#KEYCLOAK_ZAMMAD_BASE_URL', zammad_base_url)
  100. client.gsub!('#KEYCLOAK_ZAMMAD_CERTIFICATE', security[:cert])
  101. client
  102. end
  103. it 'is successful' do
  104. login_saml
  105. visit saml_realm_zammad_accounts
  106. expect(page).to have_text('John Doe')
  107. logout_saml
  108. visit saml_realm_zammad_accounts
  109. expect(page).to have_text('Sign in')
  110. end
  111. end
  112. end
  113. describe 'SP login and IDP logout' do
  114. before do
  115. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
  116. end
  117. it 'is successful' do
  118. login_saml
  119. visit saml_realm_zammad_accounts
  120. click_on 'John Doe'
  121. find('span', text: 'Sign out', class: 'pf-v5-c-menu__item-text').click
  122. visit '/'
  123. expect(page).to have_current_route('login')
  124. find_by_id('app')
  125. end
  126. end
  127. describe "use custom user attribute 'uid' as uid_attribute" do
  128. before do
  129. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, uid_attribute: 'uid')
  130. end
  131. it 'is successful' do
  132. login_saml
  133. user = User.find_by(email: 'john.doe@saml.example.com')
  134. expect(user.login).to eq('5f8179df-db5e-415c-8090-6cc3634d86b6')
  135. logout_saml
  136. end
  137. end
  138. describe 'use unspecified (IDP provided) name identifier' do
  139. before do
  140. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
  141. end
  142. it 'is successful' do
  143. login_saml
  144. user = User.find_by(email: 'john.doe@saml.example.com')
  145. expect(user.login).to eq('john.doe')
  146. logout_saml
  147. end
  148. end
  149. describe 'SAML logout without IDP SLO service URL' do
  150. before do
  151. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, idp_slo_service_url: false)
  152. end
  153. it 'is successful' do
  154. login_saml
  155. user = User.find_by(email: 'john.doe@saml.example.com')
  156. expect(user.login).to eq('john.doe@saml.example.com')
  157. logout_saml
  158. visit saml_realm_zammad_accounts
  159. expect(page).to have_text('John Doe')
  160. end
  161. end
  162. describe 'Mobile View', app: :mobile do
  163. before do
  164. skip 'Skip mobile tests enforced.' if ENV['SKIP_MOBILE_TESTS']
  165. end
  166. context 'when login is tested' do
  167. before do
  168. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
  169. end
  170. it 'is successful' do
  171. login_saml(app: 'mobile')
  172. visit saml_realm_zammad_accounts
  173. check_mobile_logout_saml
  174. end
  175. end
  176. context 'when logout is tested' do
  177. before do
  178. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
  179. end
  180. it 'is successful' do
  181. login_saml(app: 'mobile')
  182. visit '/account', app: :mobile
  183. click_on('Sign out')
  184. wait.until do
  185. expect(page).to have_button('Sign in')
  186. end
  187. visit saml_realm_zammad_accounts
  188. expect(page).to have_text('Sign in')
  189. end
  190. end
  191. context 'when saml user already exists with agent role' do
  192. before do
  193. Setting.set('auth_third_party_auto_link_at_inital_login', true)
  194. create(:agent, email: 'john.doe@saml.example.com', login: 'john.doe', firstname: 'John', lastname: 'Doe')
  195. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
  196. end
  197. it 'is successful' do
  198. login_saml(app: 'mobile')
  199. visit saml_realm_zammad_accounts
  200. check_mobile_logout_saml
  201. end
  202. end
  203. context 'when logout is tested without IDP SLO service URL' do
  204. before do
  205. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, idp_slo_service_url: false)
  206. end
  207. it 'is successful' do
  208. login_saml(app: 'mobile')
  209. visit '/account', app: :mobile
  210. click_on('Sign out')
  211. wait.until do
  212. expect(page).to have_button('Sign in')
  213. end
  214. visit saml_realm_zammad_accounts
  215. check_mobile_logout_saml
  216. end
  217. end
  218. end
  219. end