{"record":{"id":"92614f9b5dc1649b","repo":"instructure/canvas-lms","slug":"refresh-window-exceeded","errorCode":null,"errorMessage":"refresh window exceeded","messagePattern":"refresh window exceeded","errorType":"exception","errorClass":"CanvasSecurity::ServicesJwt::InvalidRefresh","httpStatus":null,"severity":"error","filePath":"gems/canvas_security/lib/canvas_security/services_jwt.rb","lineNumber":144,"sourceCode":"    if root_account_uuid\n      payload[:root_account_uuid] = root_account_uuid\n    end\n    generate(payload, base64:, symmetric:, encrypt:)\n  end\n\n  def self.refresh_for_user(jwt, domain, user, real_user: nil, symmetric: false)\n    begin\n      payload = new(jwt, wrapped: false).original_token(ignore_expiration: true)\n    rescue JSON::JWT::InvalidFormat\n      raise InvalidRefresh, \"invalid token\"\n    end\n\n    if refresh_invalid_for_user?(payload, domain, user, real_user)\n      raise InvalidRefresh, \"token does not match user and domain\"\n    end\n\n    if past_refresh_window?(payload[:exp])\n      raise InvalidRefresh, \"refresh window exceeded\"\n    end\n\n    if payload[:context_type].present?\n      context = payload[:context_type].constantize.find(payload[:context_id])\n    end\n\n    for_user(domain,\n             user,\n             real_user:,\n             workflows: payload[:workflows],\n             context:,\n             symmetric:)\n  end\n\n  def self.create_payload(payload_data)\n    if payload_data[:sub].nil?\n      raise ArgumentError, \"Cannot generate a services JWT without a 'sub' entry\"\n    end","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/gems/canvas_security/lib/canvas_security/services_jwt.rb#L126-L162","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nnew_token = ServicesJwt.refresh(old_token, domain: domain, user: user) rescue nil\n// after\nif ServicesJwt.new(old_token).expired? && past_window?\n  new_token = ServicesJwt.for_user(user, domain: domain).token\nelse\n  new_token = ServicesJwt.refresh(old_token, domain: domain, user: user)\nend","handlingStrategy":"try-catch","validationCode":"payload = CanvasSecurity::ServicesJwt.new(jwt).original_token(ignore_expiration: true)\nrefreshable = payload[:exp] && (payload[:exp] - Time.zone.now.to_i) < CanvasSecurity::ServicesJwt::REFRESH_WINDOW","typeGuard":"def within_refresh_window?(payload)\n  exp = payload.is_a?(Hash) && payload[:exp]\n  exp && (Time.zone.now.to_i - exp) <= CanvasSecurity::ServicesJwt::REFRESH_WINDOW\nend","tryCatchPattern":"begin\n  new_token = CanvasSecurity::ServicesJwt.refresh(jwt, domain: domain, user: user)\nrescue CanvasSecurity::ServicesJwt::InvalidRefresh\n  new_token = CanvasSecurity::ServicesJwt.for_user(user, domain: domain).token\nend","preventionTips":["Refresh tokens before they expire (proactive renewal)","Add clock skew tolerance on clients","Monitor for refresh loops after expiry","Keep NTP synchronized across services"],"tags":["jwt","token-expiration","authentication","token-refresh"],"backgroundTag":"jwt-token-expired","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}