discourse/lib/webauthn/security_key_authentication_service.rb
Martin Brennan 68d35b14f4 FEATURE: Webauthn authenticator management with 2FA login (Security Keys) (#8099)
Adds 2 factor authentication method via second factor security keys over [web authn](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API).

Allows a user to authenticate a second factor on login, login-via-email, admin-login, and change password routes. Adds registration area within existing user second factor preferences to register multiple security keys. Supports both external (yubikey) and built-in (macOS/android fingerprint readers).
2019-10-01 19:08:41 -07:00

88 lines
4.3 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
require 'cose'
module Webauthn
class SecurityKeyAuthenticationService < SecurityKeyBaseValidationService
##
# 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
return false if @params.blank?
# 3. Identify the user being authenticated and verify that this user is the
# owner of the public key credential source credentialSource identified by credential.id:
security_key = UserSecurityKey.find_by(credential_id: @params[:credentialId])
raise(NotFoundError, I18n.t('webauthn.validation.not_found_error')) if security_key.blank?
raise(OwnershipError, I18n.t('webauthn.validation.ownership_error')) if security_key.user != @current_user
# 4. Using credential.id (or credential.rawId, if base64url encoding is inappropriate for your use case),
# look up the corresponding credential public key and let credentialPublicKey be that credential public key.
public_key = security_key.public_key
# 5. Let cData, authData and sig denote the value of credentials response's clientDataJSON, authenticatorData, and signature respectively.
# 6. Let JSONtext be the result of running UTF-8 decode on the value of cData.
# 7. Let C, the client data claimed as used for the signature, be the result of running an implementation-specific JSON parser on JSONtext.
client_data
# 8. Verify that the value of C.type is the string webauthn.get.
validate_webauthn_type(::Webauthn::ACCEPTABLE_AUTHENTICATION_TYPE)
# 9. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
validate_challenge
# 10. Verify that the value of C.origin matches the Relying Party's origin.
validate_origin
# 11. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection
# over which the attestation was obtained. If Token Binding was used on that TLS connection, also verify
# that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
# Not using this right now.
# 12. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
validate_rp_id_hash
# 13. Verify that the User Present bit of the flags in authData is set.
# https://blog.bigbinary.com/2011/07/20/ruby-pack-unpack.html
#
# bit 0 is the least significant bit - LSB first
#
# 14. If user verification is required for this registration, verify that
# the User Verified bit of the flags in authData is set.
validate_user_verification
# 15. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator
# extension outputs in the extensions in authData are as expected, considering the client extension input
# values that were given in options.extensions and any specific policy of the Relying Party regarding
# unsolicited extensions, i.e., those that were not specified as part of options.extensions. In the
# general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use.
# Not using this right now.
# 16. Let hash be the result of computing a hash over response.clientDataJSON using SHA-256.
client_data_hash
# 17. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData and hash.
cose_key = COSE::Key.deserialize(Base64.decode64(security_key.public_key))
if !cose_key.to_pkey.verify(COSE::Algorithm.find(cose_key.alg).hash_function, signature, auth_data + client_data_hash)
raise(PublicKeyError, I18n.t('webauthn.validation.public_key_error'))
end
# Success! Update the last used at time for the key.
security_key.update(last_used: Time.zone.now)
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