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

invalid_authorization_code

invalid_authorization_code

Error message

invalid_authorization_code

What it means

Canvas::OAuth::RequestError :invalid_authorization_code is raised in Canvas::OAuth::GrantTypes::AuthorizationCode#validate_type when the authorization code supplied to the OAuth2 token endpoint does not correspond to a valid, unexpired code. The provider looks up the code via @provider.token_for(@opts[:code]); if the resulting token is not for a valid code (@_token.is_for_valid_code? is false), this error is thrown. This protects the token exchange from replayed, expired, or forged authorization codes.

Solutions

  1. Request a fresh authorization code by redirecting the user through /login/oauth2/auth again, then exchange it immediately.
  2. Ensure the code is exchanged exactly once and never cached or retried after a successful exchange.
  3. Verify the redirect/token exchange happens against the same Canvas environment (and shard) that issued the code.
  4. Check server clocks and that no proxy is stripping or rewriting the code parameter.

Example fix

// before: reusing a stored code
token = exchangeCode(storedCode) // raises invalid_authorization_code on second use

// after: always exchange a freshly received code, once
const code = new URL(redirectUrl).searchParams.get('code')
if (code && !usedCodes.has(code)) {
  usedCodes.add(code)
  token = await exchangeCode(code)
}
Defensive patterns

Strategy: retry

Validate before calling

function canExchange(code) { return typeof code === 'string' && code.length > 0 && !usedCodes.has(code) && (codeIssuedAt && Date.now() - codeIssuedAt < CODE_TTL_MS) }

Type guard

function hasAuthCode(params) { return typeof params.code === 'string' && params.code.trim() !== '' }

Try / catch

try {
  token = await exchangeCode(code)
} catch (e) {
  if (e.body?.error === 'invalid_authorization_code') {
    // code expired/replayed: restart the authorization flow
    return redirectToAuthorize()
  }
  throw e
}

Prevention

When it happens

Trigger: POST to /login/oauth2/token with grant_type=authorization_code where the 'code' param is expired, already redeemed (codes are single-use and expired via Canvas::OAuth::Token.expire_code), belongs to a different shard, was tampered with, or is missing/malformed so token_for returns a token that fails is_for_valid_code?.

Common situations: Client retries a token exchange after already successfully redeeming the code; user took too long between authorize redirect and token exchange (code TTL elapsed); environment mismatch (code issued on staging, exchanged on production); clock skew or multi-shard setups where the code was created on a different shard.

Related errors


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

Appendix: source

Thrown at lib/canvas/oauth/grant_types/authorization_code.rb:16

# 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)