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

invalid_redirect

invalid_redirect

Error message

invalid_redirect

What it means

Canvas raises Canvas::OAuth::RequestError with :invalid_redirect in the /login/oauth2/auth flow when the redirect_uri parameter does not exactly match a redirect URI registered on the developer key (provider.has_valid_redirect? returns false). This prevents open-redirect/token interception per the OAuth2 spec.

Solutions

  1. Add the exact redirect_uri your app uses to the developer key's Redirect URIs list in Canvas admin
  2. Compare byte-for-byte: scheme, host, port, path and trailing slash of the sent redirect_uri vs the registered one
  3. Register both http://localhost:PORT/... and production URIs if you develop locally
  4. Re-check after environment/proxy changes that the app's computed redirect_uri did not change (e.g. https offloading)

Example fix

// before
const redirectUri = 'https://myapp.example.com/oauth/callback' // not registered
// after
// register 'https://myapp.example.com/oauth/callback' on the developer key first
const redirectUri = new URL('/oauth/callback', window.location.origin).href // exact match saved in Canvas admin
Defensive patterns

Strategy: validation

Validate before calling

function matchesRegisteredRedirect(uri) {
  const u = new URL(uri);
  return registeredRedirects.some(r => { const v = new URL(r); return v.protocol === u.protocol && v.host === u.host && (v.pathname.replace(/\/$/, '') === u.pathname.replace(/\/$/, '') || v.pathname === u.pathname); });
}
if (!matchesRegisteredRedirect(redirectUri)) console.warn('redirect_uri not registered on developer key:', redirectUri);

Try / catch

try { await startOAuth({ redirectUri }); } catch (e) { if (e.error === 'invalid_redirect') { console.error('redirect_uri mismatch; registered URIs:', registeredRedirects); } throw e; }

Prevention

When it happens

Trigger: Calling /login/oauth2/auth with a redirect_uri that is absent from the key's registered redirect URIs, omitted when the key requires one, or differing only in trailing slash, scheme (http vs https), port, or case.

Common situations: Local development on localhost:3000 while the key only lists the production URL, adding/removing a trailing slash, switching from http to https behind a proxy, or forgetting to save the redirect URI when creating the developer key.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


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

Appendix: source

Thrown at app/controllers/oauth2_provider_controller.rb:53

      # something basic.
      return render
    end

    scopes = (params[:scope] || params[:scopes] || "").split

    provider = Canvas::OAuth::Provider.new(
      params[:client_id],
      params[:redirect_uri],
      scopes,
      params[:purpose],
      pkce: {
        code_challenge: params[:code_challenge],
        code_challenge_method: params[:code_challenge_method]
      }
    )

    raise Canvas::OAuth::RequestError, :invalid_client_id unless provider.has_valid_key?
    raise Canvas::OAuth::RequestError, :invalid_redirect unless provider.has_valid_redirect?

    session[:oauth2] = provider.session_hash
    session[:oauth2][:state] = params[:state] if params.key?(:state)
    session[:oauth2][:nonce] = params[:nonce] if params.key?(:nonce)

    if provider.key.require_scopes? && !provider.valid_scopes?
      return redirect_to Canvas::OAuth::Provider.final_redirect(self,
                                                                state: params[:state],
                                                                error: "invalid_scope",
                                                                error_description: "A requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. " \
                                                                                   "The following scopes were requested, but not granted: #{provider.missing_scopes.to_sentence(locale: :en)}")
    end

    unless provider.key.authorized_for_account?(@domain_root_account)
      return redirect_to Canvas::OAuth::Provider.final_redirect(self,
                                                                state: params[:state],
                                                                error: "unauthorized_client",
                                                                error_description: "Client does not have access to the specified Canvas account.")

View on GitHub (pinned to 1c9f0bb801)