{"record":{"id":"13c252eb8c6d215f","repo":"instructure/canvas-lms","slug":"invalid-refresh-token","errorCode":"invalid_refresh_token","errorMessage":"invalid_refresh_token","messagePattern":"invalid_refresh_token","errorType":"error_code","errorClass":"Canvas::OAuth::RequestError","httpStatus":null,"severity":"error","filePath":"lib/canvas/oauth/grant_types/refresh_token.rb","lineNumber":22,"sourceCode":"  module GrantTypes\n    class RefreshToken < BaseType\n      def supported_type?\n        true\n      end\n\n      # Access tokens obtained by public clients through PKCE should\n      # be refreshed using this grant type\n      def allow_public_client?\n        true\n      end\n\n      private\n\n      def validate_type\n        raise Canvas::OAuth::RequestError, :refresh_token_not_supplied unless @opts[:refresh_token]\n\n        @_token = @provider.token_for_refresh_token(@opts[:refresh_token])\n        raise Canvas::OAuth::RequestError, :invalid_refresh_token unless @_token\n        raise Canvas::OAuth::RequestError, :incorrect_client unless @_token.access_token.developer_key_id == @_token.key.id\n      end\n\n      def generate_token\n        @_token.access_token.regenerate_access_token\n\n        if provider.key.public_client?\n          # Access tokens for public clients have a (default) two-hour rolling window\n          # in which tokens are eligible for refresh. When a refresh action is take for\n          # a public client, extend that window by another two hours.\n          @_token.access_token.set_permanent_expiration\n\n          # For better token security, force public clients to rotate refresh tokens\n          # after each use. This helps mitigate the risk of a leaked refresh token.\n          @_token.access_token.generate_refresh_token(overwrite: true)\n          @_token.access_token.save\n        end\n","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/canvas/oauth/grant_types/refresh_token.rb#L4-L40","documentation":"Canvas's OAuth2 refresh_token grant raises Canvas::OAuth::RequestError with code invalid_refresh_token when @provider.token_for_refresh_token cannot find an AccessToken matching the supplied refresh_token. The refresh token is unknown, expired, already rotated (public clients rotate on every use), or revoked.","triggerScenarios":"POST to /login/oauth2/token with grant_type=refresh_token and a refresh_token value that matches no token: token was regenerated by a previous refresh on a public (PKCE) client, the token expired or was deleted, the value was truncated/URL-decoded incorrectly, or it belongs to a different Canvas environment.","commonSituations":"Public-client apps reusing the old refresh token after Canvas rotated it (generate_refresh_token overwrite: true in generate_token); replaying a refresh token after user token revocation; environment mismatch (dev token used against prod); storing the token with lossy encoding (plus signs, trimming).","solutions":["Use the latest refresh_token from the most recent token response — for public clients it rotates on every refresh, so always overwrite the stored value.","If rotated/lost, redirect the user through the OAuth authorization flow again to obtain a fresh token pair.","Verify you are calling the same Canvas host/account that issued the token and that the user hasn't revoked the token.","Check storage/transport of the token for encoding damage (don't trim, correctly URL-encode form bodies)."],"exampleFix":"// before\nrefreshWith(oldRefreshToken) // rotated token, now invalid\n// after\nconst resp = await refreshWith(current.refresh_token)\ncurrent = { access_token: resp.access_token, refresh_token: resp.refresh_token }","handlingStrategy":"try-catch","validationCode":"unless stored_refresh_token && stored_refresh_token == latest_token_response.refresh_token\n  reauthorize\nend","typeGuard":"def plausible_refresh_token?(t)\n  t.is_a?(String) && t.length > 20\nend","tryCatchPattern":"begin\n  token = refresh_access_token(stored_refresh_token)\nrescue Canvas::OAuth::RequestError => e\n  if e.message.to_s == 'invalid_refresh_token'\n    clear_stored_tokens\n    redirect_to_oauth_authorization\n  end\nend","preventionTips":["For public (PKCE) clients, always overwrite the stored refresh token with the one from each refresh response","Handle invalid_refresh_token by re-running the authorization flow, not by retrying","Store tokens losslessly (no trimming/re-encoding) and on the correct environment","Detect user revocation and prompt re-auth rather than looping on refresh"],"tags":["oauth2","canvas-lms","refresh-token","token-expired","token-rotation"],"backgroundTag":"oauth-token-exchange-failed","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"}