saml_spec.rb 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  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. end
  49. # Shared_examples does not work.
  50. def login_saml(app: 'desktop')
  51. case app
  52. when 'desktop'
  53. visit '/#login'
  54. find('.auth-provider--saml').click
  55. when 'mobile'
  56. visit '/login', app: :mobile
  57. find('.icon-mobile-saml').click
  58. end
  59. login_saml_keycloak
  60. check_logged_in(app: app)
  61. end
  62. def login_saml_keycloak
  63. find_by_id('kc-form')
  64. expect(page).to have_current_path(%r{/realms/zammad/protocol/saml\?SAMLRequest=.+})
  65. expect(page).to have_css('#kc-form-login')
  66. within '#kc-form-login' do
  67. fill_in 'username', with: 'john.doe'
  68. fill_in 'password', with: 'test'
  69. click_button
  70. end
  71. end
  72. def check_logged_in(app: 'desktop')
  73. find_by_id('app')
  74. case app
  75. when 'desktop'
  76. expect(page).to have_current_route('ticket/view/my_tickets')
  77. when 'mobile'
  78. # FIXME: Workaround because the redirect to the mobile app is not working due to a not set HTTP Referer in Capybara.
  79. visit '/', app: :mobile
  80. expect(page).to have_text('Home')
  81. end
  82. end
  83. def logout_saml
  84. await_empty_ajax_queue
  85. logout
  86. expect_current_route 'login'
  87. find_by_id('app')
  88. end
  89. describe 'SP login and SP logout' do
  90. before do
  91. set_saml_config
  92. end
  93. it 'is successful' do
  94. login_saml
  95. visit saml_realm_zammad_accounts
  96. expect(page).to have_css('#landingSignOutButton')
  97. find_by_id('landingWelcomeMessage')
  98. logout_saml
  99. visit saml_realm_zammad_accounts
  100. expect(page).to have_no_css('#landingSignOutButton')
  101. find_by_id('landingWelcomeMessage')
  102. end
  103. end
  104. describe 'SP login and IDP logout' do
  105. before do
  106. set_saml_config
  107. end
  108. it 'is successful' do
  109. login_saml
  110. visit saml_realm_zammad_accounts
  111. find_by_id('landingWelcomeMessage')
  112. find('#landingSignOutButton').click
  113. visit '/'
  114. expect(page).to have_current_route('login')
  115. find_by_id('app')
  116. end
  117. end
  118. describe "use custom user attribute 'uid' as uid_attribute" do
  119. before do
  120. set_saml_config(uid_attribute: 'uid')
  121. end
  122. it 'is successful' do
  123. login_saml
  124. user = User.find_by(email: 'john.doe@saml.example.com')
  125. expect(user.login).to eq('73f7c02f-77b1-4cb7-9a2a-0e7a3aeeda52')
  126. logout_saml
  127. end
  128. end
  129. describe 'use unspecified (IDP provided) name identifier' do
  130. before do
  131. set_saml_config(name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
  132. end
  133. it 'is successful' do
  134. login_saml
  135. user = User.find_by(email: 'john.doe@saml.example.com')
  136. expect(user.login).to eq('john.doe')
  137. logout_saml
  138. end
  139. end
  140. describe 'SAML logout without IDP SLO service URL' do
  141. before do
  142. set_saml_config(idp_slo_service_url: false)
  143. end
  144. it 'is successful' do
  145. login_saml
  146. user = User.find_by(email: 'john.doe@saml.example.com')
  147. expect(user.login).to eq('john.doe@saml.example.com')
  148. logout_saml
  149. visit saml_realm_zammad_accounts
  150. expect(page).to have_css('#landingSignOutButton')
  151. end
  152. end
  153. describe 'Mobile View', app: :mobile do
  154. context 'when login is tested' do
  155. before do
  156. set_saml_config
  157. end
  158. it 'is successful' do
  159. login_saml(app: 'mobile')
  160. visit saml_realm_zammad_accounts
  161. find('#landingMobileKebabButton').click
  162. expect(page).to have_css('#landingSignOutLink')
  163. end
  164. end
  165. context 'when logout is tested' do
  166. before do
  167. set_saml_config
  168. end
  169. it 'is successful' do
  170. login_saml(app: 'mobile')
  171. visit '/account', app: :mobile
  172. click_button('Sign out')
  173. wait.until do
  174. expect(page).to have_button('Sign in')
  175. end
  176. visit saml_realm_zammad_accounts
  177. find('#landingMobileKebabButton').click
  178. expect(page).to have_no_css('#landingSignOutLink')
  179. find_by_id('landingWelcomeMessage')
  180. end
  181. end
  182. context 'when saml user already exists with agent role' do
  183. before do
  184. Setting.set('auth_third_party_auto_link_at_inital_login', true)
  185. create(:agent, email: 'john.doe@saml.example.com', login: 'john.doe', firstname: 'John', lastname: 'Doe')
  186. set_saml_config
  187. end
  188. it 'is successful' do
  189. login_saml(app: 'mobile')
  190. visit saml_realm_zammad_accounts
  191. find('#landingMobileKebabButton').click
  192. expect(page).to have_css('#landingSignOutLink')
  193. end
  194. end
  195. context 'when logout is tested without IDP SLO service URL' do
  196. before do
  197. set_saml_config(idp_slo_service_url: false)
  198. end
  199. it 'is successful' do
  200. login_saml(app: 'mobile')
  201. visit '/account', app: :mobile
  202. click_button('Sign out')
  203. wait.until do
  204. expect(page).to have_button('Sign in')
  205. end
  206. visit saml_realm_zammad_accounts
  207. find('#landingMobileKebabButton').click
  208. expect(page).to have_css('#landingSignOutLink')
  209. end
  210. end
  211. end
  212. end