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
- Request a fresh authorization code by redirecting the user through /login/oauth2/auth again, then exchange it immediately.
- Ensure the code is exchanged exactly once and never cached or retried after a successful exchange.
- Verify the redirect/token exchange happens against the same Canvas environment (and shard) that issued the code.
- 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
- Exchange the code immediately upon redirect; never cache or reuse it.
- Mark codes as consumed in your own state to avoid duplicate token requests.
- Keep authorize and token exchange on the same Canvas host/account.
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)