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
- Inspect the incoming token string — verify it is a complete three-part JWT (header.payload.signature)
- Fix the client to send the token unmodified (check header/cookie encoding and any proxy rewrites)
- Rescue CanvasSecurity::ServicesJwt::InvalidRefresh and return 401 so the client re-authenticates
- 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
- Validate token shape before sending to refresh_for_user
- Return 401 on InvalidRefresh so clients re-authenticate
- Check proxies/cookie encoding don't corrupt JWTs
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Cannot decode nil token string
- Cannot generate a symmetric, non-encrypted JWT
- Must have a domain and a user to build a JWT
- A new_id, '# ', referenced an existing # and the # with #…
- A new_integration_id, '#
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)