mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-04-30 11:07:08 +08:00
**Currently, passkey registration has two bugs:** 1. Registration fails with an HTTP 500 for authenticators that include extension data (i.e. `hmac-secret` in their attestation response) because we slice all bytes after the credential ID the public key. 2. Registration fails with `NotAllowedError` on some hardware keys (i.e. Solo 2) because `pubKeyCredParams` includes invalid HMAC symmetric algorithms from the `COSE` gem, which strict authenticator firmware rejects. **This fix:** 1. Uses `CBOR::Unpacker` streaming decode to read exactly one `CBOR` object from the byte stream, stopping before any trailing extension data. Also adds `COSE::MalformedKeyError` to the rescue block so future failures return a proper error response. 2. Replaces the blanket `COSE::Algorithm.registered_algorithm_ids` with an explicit list of asymmetric signature algorithms valid for `WebAuthn`. Meta bug report: https://meta.discourse.org/t/cant-set-up-passkey-on-any-discourse/397642/
131 lines
3 KiB
Ruby
131 lines
3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module DiscourseWebauthn
|
|
ACCEPTABLE_REGISTRATION_TYPE = "webauthn.create"
|
|
ACCEPTABLE_AUTHENTICATION_TYPE = "webauthn.get"
|
|
|
|
SUPPORTED_ALGORITHMS = [
|
|
-7, # ES256
|
|
-8, # EdDSA
|
|
-35, # ES384
|
|
-36, # ES512
|
|
-37, # PS256
|
|
-38, # PS384
|
|
-39, # PS512
|
|
-257, # RS256 (via freedom patch)
|
|
].freeze
|
|
VALID_ATTESTATION_FORMATS = %w[none packed fido-u2f].freeze
|
|
CHALLENGE_EXPIRY = 5.minutes
|
|
|
|
class SecurityKeyError < StandardError
|
|
end
|
|
|
|
class InvalidOriginError < SecurityKeyError
|
|
end
|
|
|
|
class InvalidRelyingPartyIdError < SecurityKeyError
|
|
end
|
|
|
|
class UserVerificationError < SecurityKeyError
|
|
end
|
|
|
|
class UserPresenceError < SecurityKeyError
|
|
end
|
|
|
|
class ChallengeMismatchError < SecurityKeyError
|
|
end
|
|
|
|
class InvalidTypeError < SecurityKeyError
|
|
end
|
|
|
|
class UnsupportedPublicKeyAlgorithmError < SecurityKeyError
|
|
end
|
|
|
|
class UnsupportedAttestationFormatError < SecurityKeyError
|
|
end
|
|
|
|
class CredentialIdInUseError < SecurityKeyError
|
|
end
|
|
|
|
class MalformedAttestationError < SecurityKeyError
|
|
end
|
|
|
|
class KeyNotFoundError < SecurityKeyError
|
|
end
|
|
|
|
class MalformedPublicKeyCredentialError < SecurityKeyError
|
|
end
|
|
|
|
class OwnershipError < SecurityKeyError
|
|
end
|
|
|
|
class PublicKeyError < SecurityKeyError
|
|
end
|
|
|
|
class UnknownCOSEAlgorithmError < SecurityKeyError
|
|
end
|
|
|
|
##
|
|
# Usage:
|
|
#
|
|
# These methods should be used in controllers where we
|
|
# are challenging the user that has a security key, and
|
|
# they must respond with a valid webauthn response and
|
|
# credentials.
|
|
#
|
|
# @param user [User] the user to stage the challenge for
|
|
# @param server_session [ServerSession] the session to store the challenge in
|
|
def self.stage_challenge(user, server_session)
|
|
::DiscourseWebauthn::ChallengeGenerator.generate.commit_to_session(
|
|
server_session,
|
|
user,
|
|
expires: CHALLENGE_EXPIRY,
|
|
)
|
|
end
|
|
|
|
##
|
|
# Clears the challenge from the user's server session.
|
|
#
|
|
# @param user [User] the user to clear the challenge for
|
|
# @param server_session [ServerSession] the session to clear the challenge from
|
|
def self.clear_challenge(user, server_session)
|
|
server_session.delete(session_challenge_key(user))
|
|
end
|
|
|
|
def self.allowed_credentials(user, server_session)
|
|
return {} if !user.security_keys_enabled?
|
|
|
|
{
|
|
allowed_credential_ids: user.second_factor_security_key_credential_ids,
|
|
challenge: challenge(user, server_session),
|
|
}
|
|
end
|
|
|
|
def self.challenge(user, server_session)
|
|
server_session[session_challenge_key(user)]
|
|
end
|
|
|
|
def self.rp_id
|
|
Rails.env.production? ? Discourse.current_hostname : "localhost"
|
|
end
|
|
|
|
def self.origin
|
|
case Rails.env
|
|
when "development"
|
|
# defaults to the Ember CLI local port
|
|
# you might need to change this and the rp_id above
|
|
# if you are using a non-default port/hostname locally
|
|
"http://localhost:4200"
|
|
else
|
|
Discourse.base_url_no_prefix
|
|
end
|
|
end
|
|
|
|
def self.rp_name
|
|
SiteSetting.title
|
|
end
|
|
|
|
def self.session_challenge_key(user)
|
|
"staged-webauthn-challenge-#{user&.id}"
|
|
end
|
|
end
|