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
- Regenerate the file URL from the source Canvas instance so the verifier is signed with the current secret
- Check the URL was not truncated/modified in transit (JWTs are long; some mailers wrap or cut them)
- If secret rotation caused mass invalidation, coordinate TTL so tokens issued pre-rotation expire before enforcement
- 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
- Never truncate or rewrite JWTs in links (email wrapping, URL shorteners)
- Use one signing secret per environment; never copy tokens across environments
- Rotate CanvasSecurity secrets with awareness of outstanding token TTLs
- Escape file URLs properly so tokens aren't mangled in HTML/emails
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Attachment verifier token id mismatch. token id: #
- Attachment verifier token expired: #
- Cannot generate a symmetric, non-encrypted JWT
- token does not match user and domain
- Access token expired
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 attachmentView on GitHub (pinned to 1c9f0bb801)