{"record":{"id":"cfe70bcf2c7d8006","repo":"instructure/canvas-lms","slug":"refresh-token-not-supplied","errorCode":"refresh_token_not_supplied","errorMessage":"refresh_token_not_supplied","messagePattern":"refresh_token_not_supplied","errorType":"error_code","errorClass":"Canvas::OAuth::RequestError","httpStatus":null,"severity":"error","filePath":"lib/canvas/oauth/grant_types/refresh_token.rb","lineNumber":19,"sourceCode":"# frozen_string_literal: true\n\nmodule Canvas::OAuth\n  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)","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/canvas/oauth/grant_types/refresh_token.rb#L1-L37","documentation":"Canvas's OAuth2 refresh_token grant raises Canvas::OAuth::RequestError with code refresh_token_not_supplied when the token request does not include a refresh_token parameter. The grant type exists solely to exchange a refresh token for a new access token, so without one there is nothing to validate.","triggerScenarios":"POST to /login/oauth2/token with grant_type=refresh_token but the body omits refresh_token (empty string or missing form field).","commonSituations":"Client code always sends grant_type but conditionally drops the refresh_token field (e.g. nil variable serialized away); using a form encoder that strips empty values; storing the refresh token in a variable that was never persisted on first login; forgetting to URL-encode so the parameter is lost.","solutions":["Include refresh_token=<the token returned by the original token response> in the token request body.","Persist the refresh_token from the initial authorization-code/PKCE token exchange before attempting a refresh.","Check your HTTP client isn't dropping empty/nil params; log the outgoing form body to confirm.","If you never received a refresh token, redo the OAuth flow — refresh tokens are only issued on the initial grant."],"exampleFix":"// before\nbody = { grant_type: 'refresh_token' }\n// after\nbody = { grant_type: 'refresh_token', refresh_token: storedRefreshToken }","handlingStrategy":"validation","validationCode":"raise ArgumentError, 'refresh_token required' if refresh_token.nil? || refresh_token.empty?\nbody = { grant_type: 'refresh_token', refresh_token: refresh_token }","typeGuard":"def refreshable?(session)\n  session.is_a?(Hash) && session[:refresh_token].is_a?(String) && !session[:refresh_token].empty?\nend","tryCatchPattern":"begin\n  token = refresh_access_token(refresh_token)\nrescue Canvas::OAuth::RequestError => e\n  reauthorize_user if e.message.to_s == 'refresh_token_not_supplied'\nend","preventionTips":["Always persist the refresh_token from the initial token exchange before using it","Send refresh_token as a form field in the POST body, not a query param","Check your HTTP client does not drop empty/nil form values","Log the outgoing request body (sans secrets) when debugging token refresh"],"tags":["oauth2","canvas-lms","refresh-token","missing-parameter"],"backgroundTag":"missing-required-argument","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"}