Safe Online Exam
Connect Canvas

Load the detector script

Add the detector via a Canvas theme JavaScript loader so it runs on quiz pages, and handle the self-hosted Canvas local-file (HTTP 422) case.

The detector runs on Canvas quiz-taking pages. A Canvas access-code field is only a challenge signal: before showing a protected SEB flow, the detector verifies the exact course and assessment against the service. It fills the access code only after Config Key proof, shows approved web tools, and detects Canvas-confirmed completion. It reaches those pages by being loaded through your Canvas account theme JavaScript.

The stable script URL is:

${TOOL_URL}/js/canvas-seb-detector.js

When an institution hides the course-navigation item from students, the detector does not rely on a rendered navigation link. On a protected assessment page it uses the student's same-origin Canvas session to page through the course's External Tools list and select the installed LTI 1.3 tool matching the configured client and deployment IDs. On a sharded Canvas instance, that list can represent the configured globally qualified client ID as its corresponding shard-local Developer Key ID; the detector recognizes that form while rejecting different, malformed, or zero IDs. If that Canvas lookup is unavailable, it retains a visible matching navigation link as a compatibility fallback. If neither is available, the launch prompt asks the student to reload and contact their instructor or Canvas administrator instead of sending them to the course home.

Keep the external app installed in the course or inherited account even when its student navigation tab is hidden. This discovery is bounded and does not change the application's Canvas OAuth scope contract.

Cloud Run release bundles can render the loader

After the Cloud Run bundle's prepare.sh stage, run ./canvas-theme-loader.sh cloudrun.env. It writes an upload-ready canvas-theme-loader.js for the configured public origin, with no secret material. Upload that generated file as the theme's desktop JavaScript rather than transcribing the detector URL by hand.

Create the loader

Upload a small loader (not the detector itself) as the theme's desktop JavaScript. Replace ${TOOL_URL} before uploading.

(function () {
  "use strict";

  const detectorUrl = "${TOOL_URL}/js/canvas-seb-detector.js";
  const assessmentPath = /^\/courses\/\d+\/(?:quizzes\/\d+\/take|assignments\/\d+)(?:\/|$)/;

  if (!assessmentPath.test(window.location.pathname)) {
    return;
  }
  if (document.querySelector('script[data-canvas-seb-detector="true"]')) {
    return;
  }

  const script = document.createElement("script");
  script.src = detectorUrl;
  script.async = true;
  script.dataset.canvasSebDetector = "true";
  document.head.appendChild(script);
})();

The pattern matches Classic Quiz /take pages and New Quiz assignment routes (including their Canvas-generated descendants). script.src must be a plain JavaScript URL string — not a Markdown link.

Upload as theme JavaScript

Upload the loader as the account theme's desktop JavaScript and apply the theme at the intended account scope.

Confirm it applies at the right scope

Theme upload capability and inherited-theme behavior vary by Canvas configuration. If the upload control is unavailable, resolve that account setting before treating the detector as installed.

Retest after Canvas changes

Retest the loader after significant Canvas theme, CSP, or quiz-rendering changes.

Self-hosted Canvas: the local-file (HTTP 422) case

Some self-hosted Canvas deployments store an uploaded theme JavaScript file as a local /accounts/:accountId/files/:fileId/download attachment. Rails can reject that response with ActionController::InvalidCrossOriginRequest (HTTP 422), so the loader never reaches the service. This is a Canvas attachment-serving limitation — not an LTI, detector, or deployment-ID failure.

Do not disable CSRF protection

When the browser console shows that 422, do not disable Canvas CSRF protection globally. Instead, point the account theme's js_overrides value at the externally hosted loader:

${TOOL_URL}/js/canvas-seb-theme-loader.js

The hosted loader keeps the same quiz-route scope and loads the full detector only on Classic Quiz /take pages and New Quiz assignment routes. Canvas's Theme Editor has no direct URL field, so set this through your provisioning/administration path and preserve it on future theme saves. For a one-off root-account recovery, run this inside the Canvas web container (replace the account ID and URL):

bundle exec rails runner '
account = Account.find(1)
current = account.brand_config || BrandConfig.default
attrs = current.attributes.slice(*BrandConfig::ATTRS_TO_INCLUDE_IN_MD5.map(&:to_s))
attrs["js_overrides"] = "https://seb.example.edu/js/canvas-seb-theme-loader.js"
replacement = BrandConfig.for(attrs.symbolize_keys)
replacement.save_unless_dup!
replacement.sync_to_s3_and_save_to_account!(nil, account)
'

For a broader deployment, configure S3-compatible attachment storage and re-upload the theme file — Canvas then serves uploaded theme assets from object storage instead of the local files controller. That is the durable fix for arbitrary uploaded theme JavaScript; the hosted loader is the smallest safe fix for this integration.

The service also exposes /api/seb/canvas-detector.js as a compatibility alias for an existing installation that uses that path. New loaders should use /js/canvas-seb-detector.js.

Verify the detector loaded

In a normal browser, open a Classic Quiz /take page and a New Quiz assignment route and confirm the detector script loads without console errors, and that ${TOOL_URL}/js/canvas-seb-detector.js returns JavaScript. If it never loads, see Troubleshooting → Detector.

Next: verify everything end to end in Roll out & operate.

On this page