saml_spec.rb 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. # Copyright (C) 2012-2024 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. # TODO: Should be replaced with tests for the new desktop-view (or the test in general should be removed outside of selenium).
  46. describe 'SP login and SP logout' do
  47. before do
  48. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, security:)
  49. end
  50. let(:security) { nil }
  51. it 'is successful' do
  52. login_saml
  53. visit saml_realm_zammad_accounts
  54. expect(page).to have_text('John Doe')
  55. logout_saml
  56. visit saml_realm_zammad_accounts
  57. expect(page).to have_text('Sign in')
  58. end
  59. context 'with client signature required and encrypted assertions enabled' do
  60. let(:security) do
  61. # generate a new private key and certificate
  62. key = OpenSSL::PKey::RSA.new(2048)
  63. cert = OpenSSL::X509::Certificate.new
  64. cert.subject = OpenSSL::X509::Name.parse('/CN=Zammad SAML Client')
  65. cert.issuer = cert.subject
  66. cert.not_before = Time.zone.now
  67. cert.not_after = (cert.not_before + (1 * 365 * 24 * 60 * 60)) # 1 year validity
  68. cert.public_key = key.public_key
  69. cert.serial = 0x0
  70. cert.version = 2
  71. ef = OpenSSL::X509::ExtensionFactory.new
  72. ef.subject_certificate = cert
  73. ef.issuer_certificate = cert
  74. cert.add_extension(ef.create_extension('keyUsage', 'digitalSignature, keyEncipherment', true))
  75. cert.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash', false))
  76. cert.add_extension(ef.create_extension('basicConstraints', 'CA:FALSE', false))
  77. cert.sign(key, OpenSSL::Digest.new('SHA256'))
  78. pem = cert.to_pem
  79. pem.gsub!('-----BEGIN CERTIFICATE-----', '')
  80. pem.gsub!('-----END CERTIFICATE-----', '')
  81. pem.delete!("\n").strip!
  82. cert = pem
  83. pem = key.to_pem
  84. pem.gsub!('-----BEGIN RSA PRIVATE KEY-----', '') # gitleaks:allow
  85. pem.gsub!('-----END RSA PRIVATE KEY-----', '') # gitleaks:allow
  86. pem.delete!("\n").strip!
  87. key = pem
  88. {
  89. cert:,
  90. key:
  91. }
  92. end
  93. let(:saml_client_json) do
  94. client = Rails.root.join('test/data/saml/zammad-client-secure.json').read
  95. client.gsub!('#KEYCLOAK_ZAMMAD_BASE_URL', zammad_base_url)
  96. client.gsub!('#KEYCLOAK_ZAMMAD_CERTIFICATE', security[:cert])
  97. client
  98. end
  99. it 'is successful' do
  100. login_saml
  101. visit saml_realm_zammad_accounts
  102. expect(page).to have_text('John Doe')
  103. logout_saml
  104. visit saml_realm_zammad_accounts
  105. expect(page).to have_text('Sign in')
  106. end
  107. end
  108. end
  109. describe 'SP login and IDP logout' do
  110. before do
  111. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
  112. end
  113. it 'is successful' do
  114. login_saml
  115. visit saml_realm_zammad_accounts
  116. click_on 'John Doe'
  117. find('a', text: 'Sign out').click
  118. visit '/'
  119. expect(page).to have_current_route('login')
  120. find_by_id('app')
  121. end
  122. end
  123. describe "use custom user attribute 'uid' as uid_attribute" do
  124. before do
  125. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, uid_attribute: 'uid')
  126. end
  127. it 'is successful' do
  128. login_saml
  129. user = User.find_by(email: 'john.doe@saml.example.com')
  130. expect(user.login).to eq('5f8179df-db5e-415c-8090-6cc3634d86b6')
  131. logout_saml
  132. end
  133. end
  134. describe 'use unspecified (IDP provided) name identifier' do
  135. before do
  136. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
  137. end
  138. it 'is successful' do
  139. login_saml
  140. user = User.find_by(email: 'john.doe@saml.example.com')
  141. expect(user.login).to eq('john.doe')
  142. logout_saml
  143. end
  144. end
  145. describe 'SAML logout without IDP SLO service URL' do
  146. before do
  147. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, idp_slo_service_url: false)
  148. end
  149. it 'is successful' do
  150. login_saml
  151. user = User.find_by(email: 'john.doe@saml.example.com')
  152. expect(user.login).to eq('john.doe@saml.example.com')
  153. logout_saml
  154. visit saml_realm_zammad_accounts
  155. expect(page).to have_text('John Doe')
  156. end
  157. end
  158. describe 'Mobile View', app: :mobile do
  159. before do
  160. skip 'Skip mobile tests enforced.' if ENV['SKIP_MOBILE_TESTS']
  161. end
  162. context 'when login is tested' do
  163. before do
  164. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
  165. end
  166. it 'is successful' do
  167. login_saml(app: 'mobile')
  168. visit saml_realm_zammad_accounts
  169. click_on 'Actions'
  170. expect(page).to have_text('Sign out')
  171. end
  172. end
  173. context 'when logout is tested' do
  174. before do
  175. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
  176. end
  177. it 'is successful' do
  178. login_saml(app: 'mobile')
  179. visit '/account', app: :mobile
  180. click_on('Sign out')
  181. wait.until do
  182. expect(page).to have_button('Sign in')
  183. end
  184. visit saml_realm_zammad_accounts
  185. expect(page).to have_text('Sign in')
  186. end
  187. end
  188. context 'when saml user already exists with agent role' do
  189. before do
  190. Setting.set('auth_third_party_auto_link_at_inital_login', true)
  191. create(:agent, email: 'john.doe@saml.example.com', login: 'john.doe', firstname: 'John', lastname: 'Doe')
  192. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:)
  193. end
  194. it 'is successful' do
  195. login_saml(app: 'mobile')
  196. visit saml_realm_zammad_accounts
  197. click_on 'Actions'
  198. expect(page).to have_text('Sign out')
  199. end
  200. end
  201. context 'when logout is tested without IDP SLO service URL' do
  202. before do
  203. saml_configure_zammad(saml_base_url:, saml_realm_zammad_descriptor:, idp_slo_service_url: false)
  204. end
  205. it 'is successful' do
  206. login_saml(app: 'mobile')
  207. visit '/account', app: :mobile
  208. click_on('Sign out')
  209. wait.until do
  210. expect(page).to have_button('Sign in')
  211. end
  212. visit saml_realm_zammad_accounts
  213. click_on 'Actions'
  214. expect(page).to have_text('Sign out')
  215. end
  216. end
  217. end
  218. end