# = Action Pack WebAuthn Public Key Credential Options # # Abstract base class for WebAuthn ceremony options. Provides shared # attributes and challenge generation for both CreationOptions (registration) # and RequestOptions (authentication). # # This class should not be instantiated directly. Use CreationOptions or # RequestOptions instead. # # == Challenge Generation # # Each options object generates a signed, expiring challenge via # +ActionPack::WebAuthn.challenge_verifier+. The challenge is Base64URL-encoded # and includes an embedded timestamp so the server can reject stale challenges. # # == Attributes # # [+user_verification+] # Controls whether user verification (biometrics/PIN) is required. One of # +:required+, +:preferred+, or +:discouraged+. Defaults to +:preferred+. # # [+relying_party+] # The RelyingParty configuration. Defaults to +ActionPack::WebAuthn.relying_party+. # # [+challenge_expiration+] # How long the challenge remains valid. Defaults vary by ceremony type # (configured in the Railtie). # class ActionPack::WebAuthn::PublicKeyCredential::Options include ActiveModel::API include ActiveModel::Attributes CHALLENGE_LENGTH = 32 USER_VERIFICATION_OPTIONS = %i[ required preferred discouraged ].freeze attribute :user_verification, default: :preferred attribute :relying_party, default: -> { ActionPack::WebAuthn.relying_party } attribute :challenge_expiration validates :user_verification, inclusion: { in: USER_VERIFICATION_OPTIONS } def initialize(attributes = {}) super self.user_verification = user_verification.to_sym end # Validates the options, raising +InvalidOptionsError+ if any are invalid. def validate! super rescue ActiveModel::ValidationError raise ActionPack::WebAuthn::InvalidOptionsError, errors.full_messages.to_sentence end # Returns a human-readable representation of the options. def inspect attributes_string = attributes.map { |name, value| "#{name}: #{value.inspect}" }.join(", ") "#<#{self.class.name} #{attributes_string}>" end # Returns a Base64URL-encoded signed challenge containing a random nonce and # an embedded timestamp. The challenge is generated once and memoized for the # lifetime of this object. # # The timestamp allows the server to reject stale challenges. The expiration # window is configurable per-ceremony via # +config.action_pack.web_authn.creation_challenge_expiration+ and # +config.action_pack.web_authn.request_challenge_expiration+, or per-instance # via the +challenge_expiration+ attribute. def challenge @challenge ||= Base64.urlsafe_encode64( ActionPack::WebAuthn.challenge_verifier.generate( Base64.strict_encode64(SecureRandom.random_bytes(CHALLENGE_LENGTH)), expires_in: challenge_expiration ), padding: false ) end end