instructure/canvas-lms · error · CanvasSecurity::ServicesJwt::InvalidRefresh
refresh window exceeded
Error message
refresh window exceeded
What it means
CanvasSecurity::ServicesJwt raises InvalidRefresh with this message when the token being refreshed was issued too long ago: past_refresh_window?(payload[:exp]) determines the refresh is outside the allowed grace period beyond the token's expiration. Services tokens can only be refreshed within a short window after expiring; older ones must be fully re-issued.
Solutions
- Stop refreshing and request a brand-new token via ServicesJwt.for_user(user, domain: ...) with a fresh authentication/session.
- Check REFRESH_WINDOW / token TTL configuration (CanvasSecurity services jwt settings) if legitimate refreshes fail quickly — clocks or env config may be off.
- Reduce retry backoff so refresh attempts happen before the window closes, or add server-side clock-skew tolerance.
- Confirm server times are NTP-synchronized; large skew can push exp outside the window immediately.
- If this fires consistently right after expiry, increase the refresh window setting for that environment.
Example fix
// before new_token = ServicesJwt.refresh(old_token, domain: domain, user: user) rescue nil // after if ServicesJwt.new(old_token).expired? && past_window? new_token = ServicesJwt.for_user(user, domain: domain).token else new_token = ServicesJwt.refresh(old_token, domain: domain, user: user) end
Defensive patterns
Strategy: try-catch
Validate before calling
payload = CanvasSecurity::ServicesJwt.new(jwt).original_token(ignore_expiration: true) refreshable = payload[:exp] && (payload[:exp] - Time.zone.now.to_i) < CanvasSecurity::ServicesJwt::REFRESH_WINDOW
Type guard
def within_refresh_window?(payload) exp = payload.is_a?(Hash) && payload[:exp] exp && (Time.zone.now.to_i - exp) <= CanvasSecurity::ServicesJwt::REFRESH_WINDOW end
Try / catch
begin new_token = CanvasSecurity::ServicesJwt.refresh(jwt, domain: domain, user: user) rescue CanvasSecurity::ServicesJwt::InvalidRefresh new_token = CanvasSecurity::ServicesJwt.for_user(user, domain: domain).token end
Prevention
- Refresh tokens before they expire (proactive renewal)
- Add clock skew tolerance on clients
- Monitor for refresh loops after expiry
- Keep NTP synchronized across services
When it happens
Trigger: Calling refresh on a services JWT whose exp is further past (or the remaining refresh allowance is smaller) than the configured refresh window (e.g. token expired hours ago, client kept retrying refresh in a loop).
Common situations: Long-lived browser tabs resuming after token expiry; background jobs replaying a cached expired token; clock skew between services making the token appear older; refresh endpoint retries after network failures eventually exceeding the window.
Related errors
- token does not match user and domain
- Attachment verifier token expired: #
- Attachment verifier token id mismatch. token id: #
- Cannot generate a services JWT without a 'sub' entry
- masquerading user not found
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/92614f9b5dc1649b.
Report an issue: GitHub.
Appendix: source
Thrown at gems/canvas_security/lib/canvas_security/services_jwt.rb:144
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],
context:,
symmetric:)
end
def self.create_payload(payload_data)
if payload_data[:sub].nil?
raise ArgumentError, "Cannot generate a services JWT without a 'sub' entry"
endView on GitHub (pinned to 1c9f0bb801)