security_keys.rb 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class Auth::TwoFactor::AuthenticationMethod::SecurityKeys < Auth::TwoFactor::AuthenticationMethod
  3. def initiate_authentication
  4. return if user_two_factor_preference_configuration.blank?
  5. return if stored_credentials.blank?
  6. configure_webauthn
  7. WebAuthn::Credential.options_for_get(allow: stored_credentials.pluck(:external_id), user_verification: 'discouraged')
  8. end
  9. def verify(payload, configuration = user_two_factor_preference_configuration)
  10. return verify_result(false) if payload.blank? || configuration.blank?
  11. configure_webauthn
  12. return registration(payload, configuration) if configuration[:type] == 'registration'
  13. verification(payload, configuration)
  14. end
  15. def initiate_configuration
  16. configure_webauthn
  17. WebAuthn::Credential.options_for_create(
  18. user: {
  19. id: WebAuthn.generate_user_id,
  20. display_name: user.login,
  21. name: user.login,
  22. },
  23. exclude: stored_credentials.pluck(:external_id),
  24. authenticator_selection: { user_verification: 'discouraged' },
  25. )
  26. end
  27. private
  28. def registration(payload, configuration)
  29. webauthn_credential = WebAuthn::Credential.from_create(payload[:credential])
  30. begin
  31. webauthn_credential.verify(payload[:challenge])
  32. # The validation would raise WebAuthn::Error so if we are here, the credentials are valid, and we can save it
  33. verify_result(true, {}, registration_configuration(webauthn_credential, configuration))
  34. rescue WebAuthn::Error => e
  35. Rails.logger.debug { "Security key registration failed: #{e.message}" }
  36. verify_result(false)
  37. end
  38. end
  39. def registration_configuration(credential, configuration)
  40. new_configuration = user_two_factor_preference_configuration || {}
  41. new_configuration[:credentials] ||= []
  42. new_configuration[:credentials].push({
  43. external_id: credential.id,
  44. public_key: credential.public_key,
  45. nickname: configuration[:nickname],
  46. sign_count: credential.sign_count.to_s, # for storage
  47. created_at: Time.zone.now,
  48. })
  49. new_configuration
  50. end
  51. def webauthn_verify!(webauthn_credential, challenge, stored_credential)
  52. webauthn_credential.verify(
  53. challenge,
  54. public_key: stored_credential[:public_key],
  55. sign_count: stored_credential[:sign_count].to_i, # for verification
  56. )
  57. end
  58. def verification(payload, configuration)
  59. webauthn_credential = WebAuthn::Credential.from_get(payload[:credential])
  60. return verify_result(false) if webauthn_credential.nil?
  61. stored_credential = find_stored_credential(configuration, webauthn_credential)
  62. return verify_result(false) if stored_credential.nil?
  63. begin
  64. webauthn_verify!(webauthn_credential, payload[:challenge], stored_credential)
  65. # The validation would raise WebAuthn::Error so if we are here, the credentials are valid, and we can save it
  66. verify_result(true, {}, verification_configuration(webauthn_credential, stored_credential, configuration))
  67. rescue WebAuthn::Error, WebAuthn::SignCountVerificationError => e
  68. Rails.logger.debug { "Security key verification failed: #{e.message}" }
  69. verify_result(false)
  70. end
  71. end
  72. def verification_configuration(webauthn_credential, stored_credential, configuration)
  73. stored_credential[:sign_count] = webauthn_credential.sign_count.to_s # for storage
  74. configuration[:credentials].map do |c|
  75. if c[:external_id].eql?(stored_credential[:external_id])
  76. return stored_credential
  77. end
  78. c
  79. end
  80. configuration
  81. end
  82. def configure_webauthn
  83. require 'webauthn' # Only load when it is actually used
  84. WebAuthn.configure do |config|
  85. config.origin = "#{Setting.get('http_type')}://#{Setting.get('fqdn')}"
  86. config.rp_name = issuer
  87. config.credential_options_timeout = 120_000
  88. end
  89. end
  90. def issuer
  91. Setting.get('organization').presence || Setting.get('product_name').presence || 'Zammad'
  92. end
  93. def verify_result(verified, configuration = {}, new_configuration = {})
  94. return { verified: false } if !verified
  95. {
  96. **configuration,
  97. verified: true,
  98. **new_configuration,
  99. }
  100. end
  101. def stored_credentials
  102. return [] if user_two_factor_preference_configuration.blank?
  103. user_two_factor_preference_configuration[:credentials] || []
  104. end
  105. def external_id_match?(stored_credential, webauthn_credential)
  106. stored_credential[:external_id].eql?(webauthn_credential.id)
  107. end
  108. def find_stored_credential(configuration, webauthn_credential)
  109. configuration[:credentials].find { |stored_credential| external_id_match?(stored_credential, webauthn_credential) }
  110. end
  111. end