instructure/canvas-lms · warning · CanvasSecurity::ServicesJwt::InvalidRefresh

invalid token

Error message

invalid token

What it means

ServicesJwt.refresh_for_user decodes the provided refresh token (ignoring expiration) to re-issue credentials. If the token string is not parseable as a JWT (JSON::JWT::InvalidFormat), it is re-raised as InvalidRefresh with message 'invalid token'; a valid-format token that doesn't match the user/domain raises the same class with a different message.

Solutions

  1. Inspect the incoming token string — verify it is a complete three-part JWT (header.payload.signature)
  2. Fix the client to send the token unmodified (check header/cookie encoding and any proxy rewrites)
  3. Rescue CanvasSecurity::ServicesJwt::InvalidRefresh and return 401 so the client re-authenticates
  4. Verify you are passing the refresh token (not the wrapped access token) where required

Example fix

// before
new_jwt = CanvasSecurity::ServicesJwt.refresh_for_user(params[:refresh_token], domain, user)
// after
begin
  new_jwt = CanvasSecurity::ServicesJwt.refresh_for_user(params[:refresh_token], domain, user)
rescue CanvasSecurity::ServicesJwt::InvalidRefresh
  render json: { error: 'invalid refresh token' }, status: :unauthorized
end
Defensive patterns

Strategy: try-catch

Validate before calling

raise ArgumentError, 'not a jwt' unless token.is_a?(String) && token.count('.') == 2

Type guard

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

Try / catch

begin
  CanvasSecurity::ServicesJwt.refresh_for_user(token, domain, user)
rescue CanvasSecurity::ServicesJwt::InvalidRefresh
  render json: { error: 'invalid token' }, status: :unauthorized
end

Prevention

When it happens

Trigger: Calling ServicesJwt.refresh_for_user(jwt, domain, user) where jwt is truncated, base64-corrupted, not a JWT at all, or signed/garbled data from a tampered request.

Common situations: Clients sending stale or manually mangled tokens; gateway/proxies truncating Authorization headers; storing tokens in cookies and losing characters through bad encoding.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at gems/canvas_security/lib/canvas_security/services_jwt.rb:136

    end
    if context
      payload[:context_type] = context.class.name
      payload[:context_id] = context.id.to_s
    end
    if audience
      payload[:aud] = audience
    end
    if root_account_uuid
      payload[:root_account_uuid] = root_account_uuid
    end
    generate(payload, base64:, symmetric:, encrypt:)
  end

  def self.refresh_for_user(jwt, domain, user, real_user: nil, symmetric: false)
    begin
      payload = new(jwt, wrapped: false).original_token(ignore_expiration: true)
    rescue JSON::JWT::InvalidFormat
      raise InvalidRefresh, "invalid token"
    end

    if refresh_invalid_for_user?(payload, domain, user, real_user)
      raise InvalidRefresh, "token does not match user and domain"
    end

    if past_refresh_window?(payload[:exp])
      raise InvalidRefresh, "refresh window exceeded"
    end

    if payload[:context_type].present?
      context = payload[:context_type].constantize.find(payload[:context_id])
    end

    for_user(domain,
             user,
             real_user:,
             workflows: payload[:workflows],

View on GitHub (pinned to 1c9f0bb801)