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

  1. Send the same client_id (developer key) in the token exchange that was used to start the /login/oauth2/auth authorization flow.
  2. Verify client_id and client_secret pair matches a single, current developer key in the Canvas account admin settings.
  3. Re-run the authorization flow after any developer key rotation so outstanding codes match the new key.
  4. 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

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)