{"record":{"id":"a3e6a8d3d07b9ea6","repo":"basecamp/fizzy","slug":"invalid-base64-encoding-in-authenticator-data","errorCode":null,"errorMessage":"Invalid base64 encoding in authenticator data","messagePattern":"Invalid base64 encoding in authenticator data","errorType":"exception","errorClass":"ActionPack::WebAuthn::InvalidResponseError","httpStatus":null,"severity":"error","filePath":"lib/action_pack/web_authn/authenticator/data.rb","lineNumber":73,"sourceCode":"  USER_VERIFIED_FLAG = 0x04\n  BACKUP_ELIGIBLE_FLAG = 0x08\n  BACKUP_STATE_FLAG = 0x10\n  ATTESTED_CREDENTIAL_DATA_FLAG = 0x40\n\n  attr_reader :bytes, :relying_party_id_hash, :flags, :sign_count, :aaguid, :credential_id, :public_key_bytes\n\n  class << self\n    # Wraps raw authenticator data into a Data instance. Accepts an existing\n    # Data object (returned as-is), a Base64URL-encoded string, or raw binary.\n    def wrap(data)\n      if data.is_a?(self)\n        data\n      else\n        data = Base64.urlsafe_decode64(data) unless data.encoding == Encoding::BINARY\n        decode(data)\n      end\n    rescue ArgumentError\n      raise ActionPack::WebAuthn::InvalidResponseError, \"Invalid base64 encoding in authenticator data\"\n    end\n\n    # Decodes raw authenticator data bytes into a Data instance, parsing the\n    # RP ID hash, flags, sign count, and (if present) attested credential data.\n    def decode(bytes)\n      bytes = bytes.bytes if bytes.is_a?(String)\n\n      minimum_length = RELYING_PARTY_ID_HASH_LENGTH + FLAGS_LENGTH + SIGN_COUNT_LENGTH\n      if bytes.length < minimum_length\n        raise ActionPack::WebAuthn::InvalidResponseError, \"Authenticator data is too short\"\n      end\n\n      position = 0\n\n      relying_party_id_hash = bytes[position, RELYING_PARTY_ID_HASH_LENGTH].pack(\"C*\")\n      position += RELYING_PARTY_ID_HASH_LENGTH\n\n      flags = bytes[position]","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/basecamp/fizzy/blob/7aabe7458060d8a1759a53b7ede39e74e6c0b20d/lib/action_pack/web_authn/authenticator/data.rb#L55-L91","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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().","Reproduce in console: Base64.urlsafe_decode64(param) must succeed — if it raises ArgumentError the value is mangled in transit, not in the library.","Check for double URL-encoding or HTML-escaping of the parameter (look for %2B, %2F, &quot; in the raw request body).","Add a cheap format pre-check on the boundary and reject with 400 so the error surfaces at the edge."],"exampleFix":"# before\ndata = ActionPack::WebAuthn::Authenticator::Data.wrap(params[:authenticator_data])\n\n# after — validate format first, then wrap\nraw = params[:authenticator_data].to_s\nunless raw.match?(%r{\\A[A-Za-z0-9_-]+={0,2}\\z})\n  return render json: { error: \"authenticator_data must be base64url\" }, status: :bad_request\nend\ndata = ActionPack::WebAuthn::Authenticator::Data.wrap(raw)","handlingStrategy":"validation","validationCode":"BASE64URL_RE = /\\A[A-Za-z0-9_-]+={0,2}\\z/\nreturn render(json: { error: 'authenticator_data must be base64url' }, status: :bad_request) unless params[:authenticator_data].to_s.match?(BASE64URL_RE)","typeGuard":"def base64url?(value)\n  value.is_a?(String) && value.match?(%r{\\A[A-Za-z0-9_-]+={0,2}\\z})\nend","tryCatchPattern":"begin\n  data = ActionPack::WebAuthn::Authenticator::Data.wrap(raw)\nrescue ActionPack::WebAuthn::InvalidResponseError => e\n  render json: { error: e.message }, status: :bad_request\nend","preventionTips":["Encode all WebAuthn binary fields as unpadded base64url on the client, consistently.","Never route binary fields through URL form-encoding without normalization of + and /.","Pre-check format at the request boundary and reject early with a field-specific message."],"tags":["webauthn","base64","encoding","passkeys"],"backgroundTag":"invalid-base64-encoding","analyzedSha":"7aabe7458060d8a1759a53b7ede39e74e6c0b20d","analyzedAt":"2026-08-21T18:33:25.349Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}