saml_spec.rb 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe 'SAML Authentication', authenticated_as: false, integration: true, required_envs: %w[KEYCLOAK_BASE_URL KEYCLOAK_ADMIN KEYCLOAK_ADMIN_PASSWORD], type: :system do
  4. # Shared/persistent variables
  5. saml_initialized = false
  6. saml_access_token = ''
  7. let(:saml_base_url) { ENV['KEYCLOAK_BASE_URL'] }
  8. let(:zammad_base_url) { "#{Capybara.app_host}:#{Capybara.current_session.server.port}" }
  9. let(:saml_auth_endpoint) { "#{saml_base_url}/realms/master/protocol/openid-connect/token" }
  10. let(:saml_auth_payload) { { username: ENV['KEYCLOAK_ADMIN'], password: ENV['KEYCLOAK_ADMIN_PASSWORD'], grant_type: 'password', client_id: 'admin-cli' } }
  11. let(:saml_client_import_endpoint) { "#{saml_base_url}/admin/realms/zammad/clients" }
  12. let(:saml_auth_headers) { { Authorization: "Bearer #{saml_access_token}" } }
  13. let(:saml_client_json) { Rails.root.join('test/data/saml/zammad-client.json').read.gsub('ZAMMAD_BASE_URL', zammad_base_url) }
  14. let(:saml_realm_zammad_descriptor) { "#{saml_base_url}/realms/zammad/protocol/saml/descriptor" }
  15. let(:saml_realm_zammad_accounts) { "#{saml_base_url}/realms/zammad/account" }
  16. # Only before(:each) can access let() variables.
  17. before do
  18. next if saml_initialized
  19. # Get auth token.
  20. response = UserAgent.post(saml_auth_endpoint, saml_auth_payload)
  21. raise 'Authentication failed' if !response.success?
  22. saml_access_token = JSON.parse(response.body)['access_token']
  23. raise 'No access_token found' if saml_access_token.blank?
  24. # Import zammad client.
  25. response = UserAgent.post(saml_client_import_endpoint, JSON.parse(saml_client_json), { headers: saml_auth_headers, json: true, jsonParseDisable: true })
  26. raise 'Authentication failed' if !response.success?
  27. saml_initialized = true
  28. end
  29. def set_saml_config(name_identifier_format: nil, uid_attribute: nil, idp_slo_service_url: true)
  30. # Setup Zammad SAML authentication.
  31. response = UserAgent.get(saml_realm_zammad_descriptor)
  32. raise 'No Zammad realm descriptor found' if !response.success?
  33. match = response.body.match(%r{<ds:X509Certificate>(?<cert>.+)</ds:X509Certificate>})
  34. raise 'No X509Certificate found' if !match[:cert]
  35. auth_saml_credentials =
  36. {
  37. display_name: 'SAML',
  38. idp_sso_target_url: "#{saml_base_url}/realms/zammad/protocol/saml",
  39. idp_cert: "-----BEGIN CERTIFICATE-----\n#{match[:cert]}\n-----END CERTIFICATE-----",
  40. name_identifier_format: name_identifier_format || 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
  41. }
  42. if idp_slo_service_url
  43. auth_saml_credentials[:idp_slo_service_url] = "#{saml_base_url}/realms/zammad/protocol/saml"
  44. end
  45. auth_saml_credentials[:uid_attribute] = uid_attribute if uid_attribute
  46. Setting.set('auth_saml_credentials', auth_saml_credentials)
  47. Setting.set('auth_saml', true)
  48. Setting.set('fqdn', zammad_base_url.gsub(%r{^https?://}, ''))
  49. end
  50. # Shared_examples does not work.
  51. def login_saml(app: 'desktop')
  52. case app
  53. when 'desktop'
  54. visit '/#login'
  55. find('.auth-provider--saml').click
  56. when 'mobile'
  57. visit '/login', app: :mobile
  58. find('.icon-mobile-saml').click
  59. end
  60. login_saml_keycloak
  61. check_logged_in(app: app)
  62. end
  63. def login_saml_keycloak
  64. find_by_id('kc-form')
  65. expect(page).to have_current_path(%r{/realms/zammad/protocol/saml\?SAMLRequest=.+})
  66. expect(page).to have_css('#kc-form-login')
  67. within '#kc-form-login' do
  68. fill_in 'username', with: 'john.doe'
  69. fill_in 'password', with: 'test'
  70. click_button
  71. end
  72. end
  73. def check_logged_in(app: 'desktop')
  74. find_by_id('app')
  75. case app
  76. when 'desktop'
  77. expect(page).to have_current_route('ticket/view/my_tickets')
  78. when 'mobile'
  79. # FIXME: Workaround because the redirect to the mobile app is not working due to a not set HTTP Referer in Capybara.
  80. visit '/', app: :mobile
  81. expect(page).to have_text('Home')
  82. end
  83. end
  84. def logout_saml
  85. await_empty_ajax_queue
  86. logout
  87. expect_current_route 'login'
  88. find_by_id('app')
  89. end
  90. describe 'SP login and SP logout' do
  91. before do
  92. set_saml_config
  93. end
  94. it 'is successful' do
  95. login_saml
  96. visit saml_realm_zammad_accounts
  97. expect(page).to have_css('#landingSignOutButton')
  98. find_by_id('landingWelcomeMessage')
  99. logout_saml
  100. visit saml_realm_zammad_accounts
  101. expect(page).to have_no_css('#landingSignOutButton')
  102. find_by_id('landingWelcomeMessage')
  103. end
  104. end
  105. describe 'SP login and IDP logout' do
  106. before do
  107. set_saml_config
  108. end
  109. it 'is successful' do
  110. login_saml
  111. visit saml_realm_zammad_accounts
  112. find_by_id('landingWelcomeMessage')
  113. find('#landingSignOutButton').click
  114. visit '/'
  115. expect(page).to have_current_route('login')
  116. find_by_id('app')
  117. end
  118. end
  119. describe "use custom user attribute 'uid' as uid_attribute" do
  120. before do
  121. set_saml_config(uid_attribute: 'uid')
  122. end
  123. it 'is successful' do
  124. login_saml
  125. user = User.find_by(email: 'john.doe@saml.example.com')
  126. expect(user.login).to eq('73f7c02f-77b1-4cb7-9a2a-0e7a3aeeda52')
  127. logout_saml
  128. end
  129. end
  130. describe 'use unspecified (IDP provided) name identifier' do
  131. before do
  132. set_saml_config(name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
  133. end
  134. it 'is successful' do
  135. login_saml
  136. user = User.find_by(email: 'john.doe@saml.example.com')
  137. expect(user.login).to eq('john.doe')
  138. logout_saml
  139. end
  140. end
  141. describe 'SAML logout without IDP SLO service URL' do
  142. before do
  143. set_saml_config(idp_slo_service_url: false)
  144. end
  145. it 'is successful' do
  146. login_saml
  147. user = User.find_by(email: 'john.doe@saml.example.com')
  148. expect(user.login).to eq('john.doe@saml.example.com')
  149. logout_saml
  150. visit saml_realm_zammad_accounts
  151. expect(page).to have_css('#landingSignOutButton')
  152. end
  153. end
  154. describe 'Mobile View', app: :mobile do
  155. context 'when login is tested' do
  156. before do
  157. set_saml_config
  158. end
  159. it 'is successful' do
  160. login_saml(app: 'mobile')
  161. visit saml_realm_zammad_accounts
  162. find('#landingMobileKebabButton').click
  163. expect(page).to have_css('#landingSignOutLink')
  164. end
  165. end
  166. context 'when logout is tested' do
  167. before do
  168. set_saml_config
  169. end
  170. it 'is successful' do
  171. login_saml(app: 'mobile')
  172. visit '/account', app: :mobile
  173. click_button('Sign out')
  174. wait.until do
  175. expect(page).to have_button('Sign in')
  176. end
  177. visit saml_realm_zammad_accounts
  178. find('#landingMobileKebabButton').click
  179. expect(page).to have_no_css('#landingSignOutLink')
  180. find_by_id('landingWelcomeMessage')
  181. end
  182. end
  183. context 'when saml user already exists with agent role' do
  184. before do
  185. Setting.set('auth_third_party_auto_link_at_inital_login', true)
  186. create(:agent, email: 'john.doe@saml.example.com', login: 'john.doe', firstname: 'John', lastname: 'Doe')
  187. set_saml_config
  188. end
  189. it 'is successful' do
  190. login_saml(app: 'mobile')
  191. visit saml_realm_zammad_accounts
  192. find('#landingMobileKebabButton').click
  193. expect(page).to have_css('#landingSignOutLink')
  194. end
  195. end
  196. context 'when logout is tested without IDP SLO service URL' do
  197. before do
  198. set_saml_config(idp_slo_service_url: false)
  199. end
  200. it 'is successful' do
  201. login_saml(app: 'mobile')
  202. visit '/account', app: :mobile
  203. click_button('Sign out')
  204. wait.until do
  205. expect(page).to have_button('Sign in')
  206. end
  207. visit saml_realm_zammad_accounts
  208. find('#landingMobileKebabButton').click
  209. expect(page).to have_css('#landingSignOutLink')
  210. end
  211. end
  212. end
  213. end