instructure/canvas-lms · error · Canvas::OAuth::RequestError

refresh_token_not_supplied

refresh_token_not_supplied

Error message

refresh_token_not_supplied

What it means

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.

Solutions

  1. Include refresh_token=<the token returned by the original token response> in the token request body.
  2. Persist the refresh_token from the initial authorization-code/PKCE token exchange before attempting a refresh.
  3. Check your HTTP client isn't dropping empty/nil params; log the outgoing form body to confirm.
  4. If you never received a refresh token, redo the OAuth flow — refresh tokens are only issued on the initial grant.

Example fix

// before
body = { grant_type: 'refresh_token' }
// after
body = { grant_type: 'refresh_token', refresh_token: storedRefreshToken }
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, 'refresh_token required' if refresh_token.nil? || refresh_token.empty?
body = { grant_type: 'refresh_token', refresh_token: refresh_token }

Type guard

def refreshable?(session)
  session.is_a?(Hash) && session[:refresh_token].is_a?(String) && !session[:refresh_token].empty?
end

Try / catch

begin
  token = refresh_access_token(refresh_token)
rescue Canvas::OAuth::RequestError => e
  reauthorize_user if e.message.to_s == 'refresh_token_not_supplied'
end

Prevention

When it happens

Trigger: POST to /login/oauth2/token with grant_type=refresh_token but the body omits refresh_token (empty string or missing form field).

Common situations: 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.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/cfe70bcf2c7d8006. Report an issue: GitHub.

Appendix: source

Thrown at lib/canvas/oauth/grant_types/refresh_token.rb:19

# frozen_string_literal: true

module Canvas::OAuth
  module GrantTypes
    class RefreshToken < BaseType
      def supported_type?
        true
      end

      # Access tokens obtained by public clients through PKCE should
      # be refreshed using this grant type
      def allow_public_client?
        true
      end

      private

      def validate_type
        raise Canvas::OAuth::RequestError, :refresh_token_not_supplied unless @opts[:refresh_token]

        @_token = @provider.token_for_refresh_token(@opts[:refresh_token])
        raise Canvas::OAuth::RequestError, :invalid_refresh_token unless @_token
        raise Canvas::OAuth::RequestError, :incorrect_client unless @_token.access_token.developer_key_id == @_token.key.id
      end

      def generate_token
        @_token.access_token.regenerate_access_token

        if provider.key.public_client?
          # Access tokens for public clients have a (default) two-hour rolling window
          # in which tokens are eligible for refresh. When a refresh action is take for
          # a public client, extend that window by another two hours.
          @_token.access_token.set_permanent_expiration

          # For better token security, force public clients to rotate refresh tokens
          # after each use. This helps mitigate the risk of a leaked refresh token.
          @_token.access_token.generate_refresh_token(overwrite: true)

View on GitHub (pinned to 1c9f0bb801)