2019-10-02 10:08:41 +08:00
|
|
|
|
# frozen_string_literal: true
|
|
|
|
|
require "cose"
|
|
|
|
|
|
2023-08-18 20:39:10 +08:00
|
|
|
|
module DiscourseWebauthn
|
2023-10-04 02:59:28 +08:00
|
|
|
|
class AuthenticationService < BaseValidationService
|
2019-10-02 10:08:41 +08:00
|
|
|
|
##
|
|
|
|
|
# See https://w3c.github.io/webauthn/#sctn-verifying-assertion for
|
|
|
|
|
# the steps followed here. Memoized methods are called in their
|
|
|
|
|
# place in the step flow to make the process clearer.
|
|
|
|
|
def authenticate_security_key
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# Steps 1-5 of this authentication flow are in the frontend at lib/webauthn.js
|
2020-01-15 18:27:12 +08:00
|
|
|
|
if @params.blank? || (!@params.is_a?(Hash) && !@params.is_a?(ActionController::Parameters))
|
2023-10-04 02:59:28 +08:00
|
|
|
|
raise(
|
|
|
|
|
MalformedPublicKeyCredentialError,
|
|
|
|
|
I18n.t("webauthn.validation.malformed_public_key_credential_error"),
|
|
|
|
|
)
|
2023-01-09 20:10:19 +08:00
|
|
|
|
end
|
2019-10-02 10:08:41 +08:00
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 6. Identify the user being authenticated and verify that this user is the
|
|
|
|
|
# owner of the public key credential source credentialSource identified by credential.id:
|
|
|
|
|
|
|
|
|
|
# 6a. If the user was identified before the authentication ceremony was initiated,
|
|
|
|
|
# verify that the identified user account contains a credential record whose id equals credential.rawId.
|
2023-10-04 02:59:28 +08:00
|
|
|
|
security_key = UserSecurityKey.find_by(credential_id: @params[:credentialId])
|
|
|
|
|
raise(KeyNotFoundError, I18n.t("webauthn.validation.not_found_error")) if security_key.blank?
|
|
|
|
|
|
|
|
|
|
if @factor_type == UserSecurityKey.factor_types[:second_factor] &&
|
|
|
|
|
(@current_user == nil || security_key.user == nil || security_key.user != @current_user)
|
2019-10-02 10:08:41 +08:00
|
|
|
|
raise(OwnershipError, I18n.t("webauthn.validation.ownership_error"))
|
2023-01-09 20:10:19 +08:00
|
|
|
|
end
|
2019-10-02 10:08:41 +08:00
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 6b. If the user was not identified before the authentication ceremony was initiated,
|
|
|
|
|
# verify that response.userHandle is present. Verify that the user account identified by response.userHandle
|
|
|
|
|
# contains a credential record whose id equals credential.rawId
|
|
|
|
|
if @factor_type == UserSecurityKey.factor_types[:first_factor] &&
|
2023-10-12 02:36:54 +08:00
|
|
|
|
Base64.decode64(@params[:userHandle]) != security_key.user.secure_identifier
|
2023-10-06 03:22:43 +08:00
|
|
|
|
raise(OwnershipError, I18n.t("webauthn.validation.ownership_error"))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# 7. No upstream step
|
|
|
|
|
# 8. No upstream step
|
2019-10-02 10:08:41 +08:00
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 9. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively.
|
|
|
|
|
# 10. Let JSONtext be the result of running UTF-8 decode on the value of cData.
|
|
|
|
|
# 11. Let C, the client data claimed as used for the signature, be the result of running an implementation-specific JSON parser on JSONtext.
|
2019-10-02 10:08:41 +08:00
|
|
|
|
client_data
|
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 12. Verify that the value of C.type is the string webauthn.get.
|
2023-08-18 20:39:10 +08:00
|
|
|
|
validate_webauthn_type(::DiscourseWebauthn::ACCEPTABLE_AUTHENTICATION_TYPE)
|
2019-10-02 10:08:41 +08:00
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 13. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
|
2019-10-02 10:08:41 +08:00
|
|
|
|
validate_challenge
|
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 14. Verify that the value of C.origin matches the Relying Party's origin.
|
2019-10-02 10:08:41 +08:00
|
|
|
|
validate_origin
|
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 15. If C.topOrigin is present:
|
|
|
|
|
# - Verify that the Relying Party expects this credential to be used within an iframe that is not same-origin with its ancestors.
|
|
|
|
|
# - Verify that the value of C.topOrigin matches the origin of a page that the Relying Party expects to be sub-framed within.
|
|
|
|
|
# We are not using this.
|
2019-10-02 10:08:41 +08:00
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 16. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
|
2019-10-02 10:08:41 +08:00
|
|
|
|
validate_rp_id_hash
|
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 17. Verify that the User Present bit of the flags in authData is set.
|
2019-10-02 10:08:41 +08:00
|
|
|
|
# https://blog.bigbinary.com/2011/07/20/ruby-pack-unpack.html
|
|
|
|
|
#
|
2023-10-04 02:59:28 +08:00
|
|
|
|
validate_user_presence
|
|
|
|
|
|
2019-10-02 10:08:41 +08:00
|
|
|
|
#
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 18. Determine whether user verification is required for this assertion.
|
|
|
|
|
# User verification SHOULD be required if, and only if, options.userVerification is set to required.
|
|
|
|
|
# If user verification was determined to be required, verify that the UV bit of the flags in authData is set.
|
|
|
|
|
# Otherwise, ignore the value of the UV flag.
|
2023-10-04 02:59:28 +08:00
|
|
|
|
validate_user_verification if @factor_type == UserSecurityKey.factor_types[:first_factor]
|
2019-10-02 10:08:41 +08:00
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 19. If the BE bit of the flags in authData is not set, verify that the BS bit is not set.
|
|
|
|
|
# Not using this right now.
|
|
|
|
|
# 20. If the credential backup state is used as part of Relying Party business logic or policy...
|
|
|
|
|
# Not using this right now.
|
|
|
|
|
# 21. Verify that the values of the client extension outputs in clientExtensionResults...
|
2019-10-02 10:08:41 +08:00
|
|
|
|
# Not using this right now.
|
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 22. Let hash be the result of computing a hash over response.clientDataJSON using SHA-256.
|
2019-10-02 10:08:41 +08:00
|
|
|
|
client_data_hash
|
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 23. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData and hash.
|
2019-10-02 10:08:41 +08:00
|
|
|
|
cose_key = COSE::Key.deserialize(Base64.decode64(security_key.public_key))
|
2020-01-02 08:14:22 +08:00
|
|
|
|
cose_algorithm = COSE::Algorithm.find(cose_key.alg)
|
|
|
|
|
|
|
|
|
|
if cose_algorithm.blank?
|
|
|
|
|
Rails.logger.error(
|
|
|
|
|
"Unknown COSE algorithm encountered. alg: #{cose_key.alg}. user_id: #{@current_user.id}. params: #{@params.inspect}",
|
|
|
|
|
)
|
|
|
|
|
raise(UnknownCOSEAlgorithmError, I18n.t("webauthn.validation.unknown_cose_algorithm_error"))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if !cose_key.to_pkey.verify(
|
|
|
|
|
cose_algorithm.hash_function,
|
|
|
|
|
signature,
|
|
|
|
|
auth_data + client_data_hash,
|
|
|
|
|
)
|
2019-10-02 10:08:41 +08:00
|
|
|
|
raise(PublicKeyError, I18n.t("webauthn.validation.public_key_error"))
|
|
|
|
|
end
|
|
|
|
|
|
2023-10-06 03:22:43 +08:00
|
|
|
|
# 24. If authData.signCount is nonzero or credentialRecord.signCount is nonzero...
|
|
|
|
|
# Not using this right now.
|
|
|
|
|
|
|
|
|
|
# 25. If response.attestationObject is present and the Relying Party wishes to verify the attestation...
|
|
|
|
|
# Not using this right now.
|
|
|
|
|
|
|
|
|
|
# 26. Success! Update the last used at time for the key (credentialRecord).
|
2019-10-02 10:08:41 +08:00
|
|
|
|
security_key.update(last_used: Time.zone.now)
|
2023-10-04 02:59:28 +08:00
|
|
|
|
|
|
|
|
|
# Return security key record so controller can use it to update the session
|
|
|
|
|
security_key
|
2019-10-02 10:08:41 +08:00
|
|
|
|
rescue OpenSSL::PKey::PKeyError
|
|
|
|
|
raise(PublicKeyError, I18n.t("webauthn.validation.public_key_error"))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def auth_data
|
|
|
|
|
@auth_data ||= Base64.decode64(@params[:authenticatorData])
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def signature
|
|
|
|
|
@signature ||= Base64.decode64(@params[:signature])
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|