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 in AuthorizationCode#validate_type when the client_id presented at the token endpoint does not match the developer key that originally issued the authorization code. The check verifies that @_token.client_id matches either the global or local id of @_token.key (the key that created the code). It prevents a different OAuth client from redeeming another client's authorization code.
Solutions
- Send the same client_id (developer key) in the token exchange that was used to start the /login/oauth2/auth authorization flow.
- Verify client_id and client_secret pair matches a single, current developer key in the Canvas account admin settings.
- Re-run the authorization flow after any developer key rotation so outstanding codes match the new key.
- Confirm multi-tenant/multi-shard routing sends the token request to the same Canvas account where the code was issued.
Example fix
// before: mismatched credentials
token = await fetch(TOKEN_URL, { body: { grant_type: 'authorization_code', code, client_id: OLD_CLIENT_ID, client_secret: OTHER_SECRET } })
// after: use the same key that initiated the authorize redirect
const CLIENT_ID = process.env.CANVAS_CLIENT_ID // the key used in /login/oauth2/auth
token = await fetch(TOKEN_URL, { body: { grant_type: 'authorization_code', code, client_id: CLIENT_ID, client_secret: process.env.CANVAS_CLIENT_SECRET } }) Defensive patterns
Strategy: validation
Validate before calling
function clientMatchesKey(clientId, authorizeClientId) { return String(clientId) === String(authorizeClientId) } // assert before calling the token endpoint Type guard
function hasValidCredentials(cfg) { return typeof cfg.clientId === 'string' && cfg.clientId !== '' && typeof cfg.clientSecret === 'string' && cfg.clientSecret !== '' } Try / catch
try {
token = await exchangeCode(code, clientId, clientSecret)
} catch (e) {
if (e.body?.error === 'incorrect_client') {
// wrong key: fail fast, surface config mismatch to the operator
throw new Error('client_id does not match the key that issued this code')
}
throw e
} Prevention
- Store exactly one (client_id, client_secret) pair per Canvas environment and use it for both authorize and token steps.
- Bind the client_id used in the authorize redirect into your session state and verify it at exchange time.
- Re-run the authorization flow after any developer key rotation.
When it happens
Trigger: POST to /login/oauth2/token with grant_type=authorization_code where the authorization code was issued to developer key A, but the request authenticates with client_id/client_secret of developer key B (the include? check on [key.global_id, key.id] fails).
Common situations: Multiple Canvas developer keys configured and the app sends the wrong client_id; a key was recreated/rotated (new key id) while old codes were outstanding; copy-pasting credentials from another environment or app; load balancer pointing token requests at a different Canvas instance than the authorize step.
Related errors
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/2655ae0d44d19979.
Report an issue: GitHub.
Appendix: source
Thrown at lib/canvas/oauth/grant_types/authorization_code.rb:17
# frozen_string_literal: true
module Canvas::OAuth
module GrantTypes
class AuthorizationCode < BaseType
def supported_type?
true
end
private
def validate_type
raise Canvas::OAuth::RequestError, :authorization_code_not_supplied unless @opts[:code]
@_token = @provider.token_for(@opts[:code])
raise Canvas::OAuth::RequestError, :invalid_authorization_code unless @_token.is_for_valid_code?
raise Canvas::OAuth::RequestError, :incorrect_client unless [@_token.key.global_id, @_token.key.id].include? @_token.client_id.to_i
end
def generate_token
@_token.create_access_token_if_needed(replace_tokens: Canvas::Plugin.value_to_boolean(@opts[:replace_tokens]))
Canvas::OAuth::Token.expire_code(@opts[:code])
@_token
end
end
end
end
View on GitHub (pinned to 1c9f0bb801)