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
- Re-fetch the attachment and generate a fresh verifier via the standard download/inline URL flow instead of reusing the old token
- Verify the URL's attachment id matches the id encoded in the verifier token (decode the JWT payload and compare :id)
- If mismatches spike, audit code that builds file URLs manually and ensure it uses the attachment's own verifier
- 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
- Always generate file URLs through Canvas helpers, never hand-assemble verifier URLs
- Never reuse a verifier across different attachments
- Compare the token id claim with the attachment global_id before use
- Disable caching of verifier-bearing URLs
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
- Attachment verifier token expired: #
- Attachment verifier token invalid: #
- token does not match user and domain
- Cannot generate a services JWT without a 'sub' entry
- Cannot generate a symmetric, non-encrypted JWT
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 accessView on GitHub (pinned to 1c9f0bb801)