basecamp/fizzy · error · ActionPack::WebAuthn::InvalidResponseError

Invalid base64 encoding in authenticator data

Error message

Invalid base64 encoding in authenticator data

What it means

ActionPack::WebAuthn::Authenticator::Data.wrap accepts an existing Data object, a Base64URL string, or raw binary. Non-binary strings go through Base64.urlsafe_decode64, which raises ArgumentError when the input contains characters outside the base64url alphabet (A–Z, a–z, 0–9, -, _); the library re-raises it as InvalidResponseError. It means the authenticator data string was corrupted, double-encoded, or in the wrong base64 variant before parsing.

Source

Thrown at lib/action_pack/web_authn/authenticator/data.rb:73

  USER_VERIFIED_FLAG = 0x04
  BACKUP_ELIGIBLE_FLAG = 0x08
  BACKUP_STATE_FLAG = 0x10
  ATTESTED_CREDENTIAL_DATA_FLAG = 0x40

  attr_reader :bytes, :relying_party_id_hash, :flags, :sign_count, :aaguid, :credential_id, :public_key_bytes

  class << self
    # Wraps raw authenticator data into a Data instance. Accepts an existing
    # Data object (returned as-is), a Base64URL-encoded string, or raw binary.
    def wrap(data)
      if data.is_a?(self)
        data
      else
        data = Base64.urlsafe_decode64(data) unless data.encoding == Encoding::BINARY
        decode(data)
      end
    rescue ArgumentError
      raise ActionPack::WebAuthn::InvalidResponseError, "Invalid base64 encoding in authenticator data"
    end

    # Decodes raw authenticator data bytes into a Data instance, parsing the
    # RP ID hash, flags, sign count, and (if present) attested credential data.
    def decode(bytes)
      bytes = bytes.bytes if bytes.is_a?(String)

      minimum_length = RELYING_PARTY_ID_HASH_LENGTH + FLAGS_LENGTH + SIGN_COUNT_LENGTH
      if bytes.length < minimum_length
        raise ActionPack::WebAuthn::InvalidResponseError, "Authenticator data is too short"
      end

      position = 0

      relying_party_id_hash = bytes[position, RELYING_PARTY_ID_HASH_LENGTH].pack("C*")
      position += RELYING_PARTY_ID_HASH_LENGTH

      flags = bytes[position]

View on GitHub (pinned to 7aabe74580)

Solutions

  1. Encode on the client as Base64URL without padding (e.g. Ruby Base64.urlsafe_encode64(..., padding: false), JS base64url helpers) using the raw bytes from response.authenticatorData / response.getAuthenticatorData().
  2. Reproduce in console: Base64.urlsafe_decode64(param) must succeed — if it raises ArgumentError the value is mangled in transit, not in the library.
  3. Check for double URL-encoding or HTML-escaping of the parameter (look for %2B, %2F, &quot; in the raw request body).
  4. Add a cheap format pre-check on the boundary and reject with 400 so the error surfaces at the edge.

Example fix

# before
data = ActionPack::WebAuthn::Authenticator::Data.wrap(params[:authenticator_data])

# after — validate format first, then wrap
raw = params[:authenticator_data].to_s
unless raw.match?(%r{\A[A-Za-z0-9_-]+={0,2}\z})
  return render json: { error: "authenticator_data must be base64url" }, status: :bad_request
end
data = ActionPack::WebAuthn::Authenticator::Data.wrap(raw)
Defensive patterns

Strategy: validation

Validate before calling

BASE64URL_RE = /\A[A-Za-z0-9_-]+={0,2}\z/
return render(json: { error: 'authenticator_data must be base64url' }, status: :bad_request) unless params[:authenticator_data].to_s.match?(BASE64URL_RE)

Type guard

def base64url?(value)
  value.is_a?(String) && value.match?(%r{\A[A-Za-z0-9_-]+={0,2}\z})
end

Try / catch

begin
  data = ActionPack::WebAuthn::Authenticator::Data.wrap(raw)
rescue ActionPack::WebAuthn::InvalidResponseError => e
  render json: { error: e.message }, status: :bad_request
end

Prevention

When it happens

Trigger: Calling Authenticator::Data.wrap (directly or via AttestationResponse/AssertionResponse with an authenticator_data param) with standard Base64 containing + or /, a value mangled by URL form-encoding, a double-encoded string, or plain garbage such as an error message body.

Common situations: JavaScript clients that use btoa() (standard base64) instead of base64url; params stripped or truncated by proxies; test fixtures hand-typed with wrong characters; passing clientDataJSON (JSON text) into a field the server expects to be base64url authenticator data.

Understand the failure class

Related errors


AI-assisted analysis of basecamp/fizzy@7aabe74580 (2026-08-21). Data as JSON: /api/errors/a3e6a8d3d07b9ea6. Report an issue: GitHub.