instructure/canvas-lms · warning

Attachment verifier token expired: #

Error message

Attachment verifier token expired: #{verifier}

What it means

When CanvasSecurity.decode_jwt raises CanvasSecurity::TokenExpired, the attachment verifier JWT has passed its expiry. decode_verifier logs this warning with the raw token, increments the attachments.token_verifier_expired metric, and returns nil, denying access. Verifiers are short-lived by design.

Solutions

  1. Regenerate the download URL so a fresh verifier is issued (re-request the file's download/inline URL)
  2. Increase the verifier TTL if legitimate links expire too quickly (CanvasSecurity JWT TTL config)
  3. Avoid persisting verifier URLs in emails/caches; link to a page that mints a fresh verifier
  4. Tell users to re-open the file from Canvas UI rather than reusing old links

Example fix

// before
<a href="<%= file.download_url %>">  # embedded with expired verifier
// after
<a href="<%= course_file_path(context, file) %>">  # fresh verifier minted per request
Defensive patterns

Strategy: validation

Validate before calling

def verifier_expired?(verifier)
  payload = CanvasSecurity.decode_jwt(verifier) rescue nil
  payload.nil? # decode_verifier returns nil for expired tokens
end

Type guard

def fresh_verifier?(verifier, ttl: 5.minutes)
  payload = CanvasSecurity.decode_jwt(verifier) rescue nil
  payload.is_a?(Hash) && payload[:exp].to_i > Time.now.to_i
end

Try / catch

begin
  body = CanvasSecurity.decode_jwt(verifier)
rescue CanvasSecurity::TokenExpired
  redirect_to fresh_download_url(attachment)
  nil
end

Prevention

When it happens

Trigger: A user follows a download/inline link whose verifier JWT was generated earlier than the token TTL (e.g. a link emailed or bookmarked, opened after expiration).

Common situations: Emailed file links opened days later; cached HTML pages embedding expired verifiers; long-running browser sessions reusing old URLs; overly short verifier TTL settings.

Understand the failure class

Related errors


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

Appendix: source

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

  # 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
  # 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)

View on GitHub (pinned to 1c9f0bb801)