instructure/canvas-lms · error · Canvas::OAuth::RequestError
incorrect_client
incorrect_client
Error message
incorrect_client
What it means
Canvas::OAuth::RequestError :incorrect_client is raised during the refresh_token grant type validation. After the refresh token is resolved, the library verifies that the stored refresh token's access token belongs to the same developer key (client) that is presenting it. It exists to prevent tokens issued to one OAuth client from being refreshed by a different client.
Solutions
- Use the client_id/client_secret of the same developer key that originally issued the refresh token
- Re-run the full OAuth authorization flow to obtain a fresh refresh token bound to the current developer key
- If the developer key was replaced, update the integration's stored credentials to the new key and re-authorize
Example fix
// before POST /login/oauth2/token?grant_type=refresh_token&client_id=NEW_KEY&client_secret=...&refresh_token=OLD_TOKEN // after POST /login/oauth2/token?grant_type=refresh_token&client_id=ORIGINAL_KEY&client_secret=...&refresh_token=OLD_TOKEN
Defensive patterns
Strategy: validation
Validate before calling
raise 'client mismatch' unless refresh_token.developer_key_id == developer_key.id
Try / catch
begin
token = provider.token_for_refresh_token(params[:refresh_token])
rescue Canvas::OAuth::RequestError => e
return render json: { error: e.error }, status: :unauthorized if e.error == :incorrect_client
end Prevention
- Store client_id together with the refresh token and assert equality before refreshing
- Re-authorize after rotating developer keys
- Keep one credentials set per environment; never share tokens across keys
When it happens
Trigger: Calling the OAuth token endpoint with grant_type=refresh_token while supplying client_id/client_secret of a developer key different from the one that originally issued the refresh token; e.g. @_token.access_token.developer_key_id != @_token.key.id.
Common situations: Rotating or replacing a Canvas developer key and reusing old refresh tokens; pointing a staging environment at production-issued refresh tokens; configuring the wrong client_id in an LTI/integration's OAuth settings.
Related errors
- invalid_client_secret
- User is from unacceptable domain
- An object of type was hidden due to insufficient scopes on…
- Attachment verifier token expired: #
- Attachment verifier token id mismatch. token id: #
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/303979816f78d821.
Report an issue: GitHub.
Appendix: source
Thrown at lib/canvas/oauth/grant_types/refresh_token.rb:23
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)
@_token.access_token.save
end
@_tokenView on GitHub (pinned to 1c9f0bb801)