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
- 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.
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
- 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
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
- invalid_refresh_token
- assertion method not supported for this grant_type
- invalid_client_id
- invalid_redirect
- polling.poll_sessions.errors.course_required
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)