instructure/canvas-lms · warning

Attachment verifier token invalid: #

Error message

Attachment verifier token invalid: #{verifier}

What it means

CanvasSecurity.decode_jwt raises CanvasSecurity::InvalidToken when the verifier JWT fails signature or format validation — it is malformed, tampered with, or signed with a different secret. decode_verifier logs this warning, increments attachments.token_verifier_invalid, and returns nil, so the attachment request is denied.

Solutions

  1. Regenerate the file URL from the source Canvas instance so the verifier is signed with the current secret
  2. Check the URL was not truncated/modified in transit (JWTs are long; some mailers wrap or cut them)
  3. If secret rotation caused mass invalidation, coordinate TTL so tokens issued pre-rotation expire before enforcement
  4. Compare environments: a verifier only validates against the instance whose signing secret issued it

Example fix

// before
verifier = params[:verifier][0, 40] # truncated token
// after
verifier = params[:verifier] # pass the full token unchanged
Defensive patterns

Strategy: validation

Validate before calling

def well_formed_verifier?(verifier)
  verifier.is_a?(String) && verifier.count('.') == 2 && verifier.length > 40
end

Type guard

def decodable_jwt?(token)
  token.is_a?(String) && token.split('.').length == 3
end

Try / catch

begin
  body = CanvasSecurity.decode_jwt(verifier)
rescue CanvasSecurity::InvalidToken
  Rails.logger.warn("invalid verifier, regenerating")
  nil
end

Prevention

When it happens

Trigger: A verifier string that is truncated, altered, forged, or signed by a different Canvas instance/secret key is passed to a file URL.

Common situations: URL truncation by email clients or chat apps breaking the JWT; a rotated CanvasSecurity secret invalidating old tokens; cross-environment token reuse (staging URL token used in production); manual URL editing.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/10815a93be3aa837. Report an issue: GitHub.

Appendix: source

Thrown at app/models/attachments/verification.rb:91

  # Returns nil if the verifier could not be decoded for whatever reason, and returns
  # a Hash of the body contents if it can.
  def decode_verifier(verifier)
    begin
      body = CanvasSecurity.decode_jwt(verifier)
      if body[:id] != attachment.global_id
        InstStatsd::Statsd.distributed_increment("attachments.token_verifier_id_mismatch")
        Rails.logger.warn("Attachment verifier token id mismatch. token id: #{body[:id]}, attachment id: #{attachment.global_id}, token: #{verifier}")
        return nil
      end

      InstStatsd::Statsd.distributed_increment("attachments.token_verifier_success")
    rescue CanvasSecurity::TokenExpired
      InstStatsd::Statsd.distributed_increment("attachments.token_verifier_expired")
      Rails.logger.warn("Attachment verifier token expired: #{verifier}")
      return nil
    rescue CanvasSecurity::InvalidToken
      InstStatsd::Statsd.distributed_increment("attachments.token_verifier_invalid")
      Rails.logger.warn("Attachment verifier token invalid: #{verifier}")
      return nil
    end

    body
  end

  # Decodes a verifier and checks the user of the verifier has permission to access
  # the attachment.
  #
  # @param verifier (String) - The verifier
  # @param permission (Symbol) - Either :read or :download
  #
  # Returns a boolean
  def valid_verifier_for_permission?(verifier, permission, root_account, session = {}, request: nil, files_domain: false)
    return false unless verifier.is_a?(String)

    # Support for legacy verifiers.
    # if we have a uuid-sized verifier that doesn't match, see whether it matches a related attachment

View on GitHub (pinned to 1c9f0bb801)