{"record":{"id":"5f1047bb2d704bca","repo":"instructure/canvas-lms","slug":"invalid-redirect","errorCode":"invalid_redirect","errorMessage":"invalid_redirect","messagePattern":"invalid_redirect","errorType":"error_code","errorClass":"Canvas::OAuth::RequestError","httpStatus":400,"severity":"error","filePath":"app/controllers/oauth2_provider_controller.rb","lineNumber":53,"sourceCode":"      # something basic.\n      return render\n    end\n\n    scopes = (params[:scope] || params[:scopes] || \"\").split\n\n    provider = Canvas::OAuth::Provider.new(\n      params[:client_id],\n      params[:redirect_uri],\n      scopes,\n      params[:purpose],\n      pkce: {\n        code_challenge: params[:code_challenge],\n        code_challenge_method: params[:code_challenge_method]\n      }\n    )\n\n    raise Canvas::OAuth::RequestError, :invalid_client_id unless provider.has_valid_key?\n    raise Canvas::OAuth::RequestError, :invalid_redirect unless provider.has_valid_redirect?\n\n    session[:oauth2] = provider.session_hash\n    session[:oauth2][:state] = params[:state] if params.key?(:state)\n    session[:oauth2][:nonce] = params[:nonce] if params.key?(:nonce)\n\n    if provider.key.require_scopes? && !provider.valid_scopes?\n      return redirect_to Canvas::OAuth::Provider.final_redirect(self,\n                                                                state: params[:state],\n                                                                error: \"invalid_scope\",\n                                                                error_description: \"A requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. \" \\\n                                                                                   \"The following scopes were requested, but not granted: #{provider.missing_scopes.to_sentence(locale: :en)}\")\n    end\n\n    unless provider.key.authorized_for_account?(@domain_root_account)\n      return redirect_to Canvas::OAuth::Provider.final_redirect(self,\n                                                                state: params[:state],\n                                                                error: \"unauthorized_client\",\n                                                                error_description: \"Client does not have access to the specified Canvas account.\")","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/controllers/oauth2_provider_controller.rb#L35-L71","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Add the exact redirect_uri your app uses to the developer key's Redirect URIs list in Canvas admin","Compare byte-for-byte: scheme, host, port, path and trailing slash of the sent redirect_uri vs the registered one","Register both http://localhost:PORT/... and production URIs if you develop locally","Re-check after environment/proxy changes that the app's computed redirect_uri did not change (e.g. https offloading)"],"exampleFix":"// before\nconst redirectUri = 'https://myapp.example.com/oauth/callback' // not registered\n// after\n// register 'https://myapp.example.com/oauth/callback' on the developer key first\nconst redirectUri = new URL('/oauth/callback', window.location.origin).href // exact match saved in Canvas admin","handlingStrategy":"validation","validationCode":"function matchesRegisteredRedirect(uri) {\n  const u = new URL(uri);\n  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); });\n}\nif (!matchesRegisteredRedirect(redirectUri)) console.warn('redirect_uri not registered on developer key:', redirectUri);","typeGuard":null,"tryCatchPattern":"try { await startOAuth({ redirectUri }); } catch (e) { if (e.error === 'invalid_redirect') { console.error('redirect_uri mismatch; registered URIs:', registeredRedirects); } throw e; }","preventionTips":["Register both localhost-dev and production redirect URIs on the key","Build redirect_uri from a single config value, not from request headers","Re-verify after proxy/TLS changes that scheme/host/port are unchanged","Avoid ad-hoc trailing-slash differences between code and registration"],"tags":["oauth2","redirect-uri","developer-key","canvas-lms"],"backgroundTag":"invalid-url-format","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}