instructure/canvas-lms · warning

Attachment verifier token id mismatch. token id: #

Error message

Attachment verifier token id mismatch. token id: #{body[:id]}, attachment id: #{attachment.global_id}, token: #{verifier}

What it means

decode_verifier decodes an attachment verifier JWT and requires the token's :id claim to equal the attachment's global_id. On mismatch it logs this warning, emits the attachments.token_verifier_id_mismatch StatsD metric, and returns nil, so valid_verifier_for_permission? fails and access is denied. It is a defensive check against using a verifier issued for a different attachment.

Solutions

  1. Re-fetch the attachment and generate a fresh verifier via the standard download/inline URL flow instead of reusing the old token
  2. Verify the URL's attachment id matches the id encoded in the verifier token (decode the JWT payload and compare :id)
  3. If mismatches spike, audit code that builds file URLs manually and ensure it uses the attachment's own verifier
  4. Check for proxies/caches serving stale signed URLs and disable caching of verifier-bearing URLs

Example fix

// before
url = "/files/#{attachment.id}/download?verifier=#{old_verifier_from_other_file}"
// after
url = attachment.public_download_url # verifier minted for THIS attachment
Defensive patterns

Strategy: validation

Validate before calling

def verifier_matches?(verifier, attachment)
  payload = CanvasSecurity.decode_jwt(verifier) rescue nil
  payload.is_a?(Hash) && payload[:id] == attachment.global_id
end

Type guard

def valid_verifier_payload?(payload, attachment)
  payload.is_a?(Hash) && payload[:id].to_i == attachment.global_id.to_i
end

Try / catch

begin
  body = CanvasSecurity.decode_jwt(verifier)
rescue CanvasSecurity::TokenExpired, CanvasSecurity::InvalidToken => e
  Rails.logger.warn("verifier rejected: #{e.class}")
  nil
end

Prevention

When it happens

Trigger: A client presents a verifier JWT (e.g. /files/:id/download?verifier=...) whose id claim points to a different attachment than the one being served — stale or hand-built URLs, copied links between files, or a token minted before an attachment was replaced.

Common situations: Bookmarked/cached download URLs reused after an attachment was recreated; course copies where the new attachment has a different global_id but an old verifier is replayed; manually constructed URLs reusing a verifier across files.

Related errors


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

Appendix: source

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

    pm = opts[:permission_map_id]
    body[:pm] = pm.to_s if pm && PERMISSION_MAPS.key?(pm)

    CanvasSecurity.create_jwt(body, opts[:expires])
  end

  # Decodes a verifier and asserts its validity (but does not check permissions!). You
  # probably want to use `valid_verifier_for_permission?`.
  #
  # @param verifier (String) - The verifier
  #
  # 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

View on GitHub (pinned to 1c9f0bb801)