two_factor_preference.rb 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rotp'
  3. require 'webauthn'
  4. FactoryBot.define do
  5. factory :'user/two_factor_preference', aliases: %i[user_two_factor_preference] do
  6. transient do
  7. user { association(:user, preferences: { two_factor_authentication: { default: method } }) }
  8. end
  9. user_id { user.id }
  10. updated_by_id { user.id }
  11. created_by_id { user.id }
  12. trait :authenticator_app do
  13. add_attribute(:method) { 'authenticator_app' }
  14. transient do
  15. secret { ROTP::Base32.random_base32 }
  16. code { ROTP::TOTP.new(secret).now }
  17. end
  18. before(:create) do
  19. Setting.set('two_factor_authentication_method_authenticator_app', true)
  20. end
  21. configuration do
  22. {
  23. secret: secret,
  24. code: code, # Store a valid code for usage from the tests.
  25. provisioning_uri: ROTP::TOTP.new(secret, issuer: 'Zammad CI').provisioning_uri(user.login),
  26. }
  27. end
  28. end
  29. trait :security_keys do
  30. add_attribute(:method) { 'security_keys' }
  31. transient do
  32. # A fake static key is enough for most of the tests.
  33. credential do
  34. {
  35. external_id: Faker::Alphanumeric.alpha(number: 70),
  36. public_key: Faker::Alphanumeric.alpha(number: 128),
  37. nickname: Faker::Lorem.unique.word,
  38. sign_count: '0',
  39. created_at: Time.zone.now,
  40. }
  41. end
  42. end
  43. before(:create) do
  44. Setting.set('two_factor_authentication_method_security_keys', true)
  45. end
  46. configuration do
  47. {
  48. credentials: [credential],
  49. }
  50. end
  51. end
  52. trait :mocked_security_keys do
  53. add_attribute(:method) { 'security_keys' }
  54. transient do
  55. page { raise NotImplementedError, 'You must provide current page object for mocking credentials' }
  56. wrong_key { false }
  57. # We can mock a WebAuthn credential only within a running browser session.
  58. # The code below is a pretty heavy "hack" to get the credential information from the Selenium virtual
  59. # authenticator, by simulating the complete registration process.
  60. # First, the create options are generated via Ruby code.
  61. # Then, a virtual authenticator instance is set up within the browser session with a mocked U2F key.
  62. # Create options are passed to JS, which triggers the registration of the key.
  63. # Finally, returned key information is processed back in Ruby, and the mocked credential can be "stored" in
  64. # the emulated two factor preferences.
  65. credential do
  66. WebAuthn.configure do |config|
  67. config.origin = "#{Setting.get('http_type')}://#{Capybara.app_host.gsub(%r{^https?://}, '')}:#{Capybara.current_session.server.port}"
  68. config.rp_name = Setting.get('organization').presence || Setting.get('product_name').presence || 'Zammad'
  69. config.credential_options_timeout = 120_000
  70. end
  71. initiate_configuration = WebAuthn::Credential.options_for_create(
  72. user: {
  73. id: WebAuthn.generate_user_id,
  74. display_name: user.login,
  75. name: user.login,
  76. },
  77. )
  78. options = Selenium::WebDriver::VirtualAuthenticatorOptions.new(protocol: :u2f, transport: :usb,
  79. resident_key: false, user_consenting: true,
  80. user_verification: true, user_verified: true)
  81. page.driver.browser.add_virtual_authenticator(options)
  82. public_key_json = JSON.generate({ publicKey: initiate_configuration.as_json.to_h })
  83. public_key_credential = page.execute_script("return webauthnJSON.create(#{public_key_json}).then((publicKeyCredential) => publicKeyCredential);")
  84. webauthn_credential = WebAuthn::Credential.from_create(public_key_credential)
  85. if wrong_key
  86. {
  87. external_id: Faker::Alphanumeric.alpha(number: 70),
  88. public_key: Faker::Alphanumeric.alpha(number: 128),
  89. nickname: Faker::Lorem.unique.word,
  90. sign_count: '0',
  91. created_at: Time.zone.now,
  92. }
  93. else
  94. {
  95. external_id: webauthn_credential.id,
  96. public_key: webauthn_credential.public_key,
  97. nickname: Faker::Lorem.unique.word,
  98. sign_count: webauthn_credential.sign_count.to_s,
  99. created_at: Time.zone.now,
  100. }
  101. end
  102. end
  103. end
  104. before(:create) do
  105. Setting.set('two_factor_authentication_method_security_keys', true)
  106. end
  107. configuration do
  108. {
  109. credentials: [credential],
  110. }
  111. end
  112. end
  113. trait :recovery_codes do
  114. add_attribute(:method) { 'recovery_codes' }
  115. transient do
  116. user { association :user }
  117. recovery_code { 'example' }
  118. end
  119. before(:create) do
  120. Setting.set('two_factor_authentication_recovery_codes', true)
  121. end
  122. configuration do
  123. {
  124. codes: [PasswordHash.crypt(recovery_code)],
  125. }
  126. end
  127. end
  128. end
  129. end