security_keys.rb 4.8 KB

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