two_factor_preference.rb 4.9 KB

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