> ## Documentation Index
> Fetch the complete documentation index at: https://docs.safeonlineexam.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Registering Safe Online Exam as a Canvas LTI 1.3 App

> Create the Canvas API OAuth key and LTI 1.3 Developer Key, install the external app, and load the SEB detector script through the Canvas account theme.

Safe Online Exam integrates with Canvas LMS through two separate Developer Keys — an API OAuth key for assessment management and a LTI 1.3 key for signed course launches — plus a theme-loaded detector script that runs on quiz pages. This page walks a Canvas root-account administrator through creating both keys, installing the external app, loading the detector through the Canvas theme, and verifying the full integration.

<Note>
  Complete [Deployment](/deployment/overview) and confirm that the service's public `TOOL_URL` is final before creating any Canvas registrations. For Docker/VPS deployments, establish DNS and TLS before this step. The Cloud Run bundle reserves the stable URL before it pauses for the Canvas steps.
</Note>

## Prerequisites

You need a Canvas root-account administrator who can manage Developer Keys, external apps, account themes, and OAuth scopes. You also need the deployed service URL and access to the deployment's secret store.

Before changing Canvas, confirm these service endpoints respond:

```bash theme={null}
curl -fsS "${TOOL_URL}/health"
curl -fsS "${TOOL_URL}/lti/config"
curl -fsS "${TOOL_URL}/.well-known/jwks.json"
curl -fsS "${TOOL_URL}/js/canvas-seb-detector.js" | head
```

The `/setup` and `/setup/guide` pages on the service provide a public role-oriented checklist. They confirm that the service responds, not that Canvas has stored the intended registration or loaded the detector.

### Values to Record

Keep a record of these values as you progress through the steps:

| Value                      | Canvas source                                                | Runtime destination                          |
| -------------------------- | ------------------------------------------------------------ | -------------------------------------------- |
| `CANVAS_DOMAIN`            | The Canvas base origin, such as `https://canvas.example.edu` | `CANVAS_DOMAIN`                              |
| `LTI_CLIENT_ID`            | LTI 1.3 Developer Key client ID                              | `LTI_CLIENT_ID`                              |
| `LTI_DEPLOYMENT_ID`        | External App deployment ID after installation                | `LTI_DEPLOYMENT_ID` when checking is enabled |
| `CANVAS_API_CLIENT_ID`     | API OAuth Developer Key client ID                            | `CANVAS_API_CLIENT_ID`                       |
| `CANVAS_API_CLIENT_SECRET` | API OAuth Developer Key secret                               | `CANVAS_API_CLIENT_SECRET`                   |
| `CANVAS_REDIRECT_URI`      | OAuth callback registration                                  | `${TOOL_URL}/api/oauth2callback`             |

<Warning>
  The LTI client ID and Canvas API OAuth client ID come from different registrations. Mixing them breaks either signed LTI launches or Canvas API authorization.
</Warning>

## Installation Steps

<Steps>
  <Step title="Create the Canvas API OAuth Developer Key">
    The application uses user-scoped Canvas OAuth tokens for assessment discovery and access-code changes. Do not replace this with a personal access token.

    In Canvas, open **Admin**, select the root account, open **Developer Keys**, and create an **API Key**. Use a recognizable name, owner email, and purpose, then set this exact redirect URI:

    ```text theme={null}
    ${TOOL_URL}/api/oauth2callback
    ```

    If the Canvas instance supports enforced scopes, allow this complete application scope set:

    ```text theme={null}
    url:GET|/api/v1/courses
    url:GET|/api/v1/courses/:course_id/quizzes
    url:GET|/api/v1/courses/:course_id/assignments
    url:GET|/api/quiz/v1/courses/:course_id/quizzes/:assignment_id
    url:PUT|/api/v1/courses/:course_id/quizzes/:id
    url:PATCH|/api/quiz/v1/courses/:course_id/quizzes/:assignment_id
    url:GET|/api/v1/login/session_token
    ```

    For the root-account administrator dashboard, also allow this administrator scope set on the **same** OAuth key:

    ```text theme={null}
    url:GET|/api/v1/accounts/:id
    url:GET|/api/v1/accounts/:account_id/permissions
    url:GET|/api/v1/accounts/:account_id/courses
    url:GET|/api/v1/accounts/:account_id/terms
    url:GET|/api/v1/courses/:id
    url:GET|/api/v1/courses/:course_id/quizzes/:id
    ```

    Every Canvas connection requests the complete scope set regardless of the user's role. This prevents a person who is an instructor in one course and a student in another from holding an incompatible role-specific grant. Canvas continues to enforce the authorizing user's actual account and course permissions.

    Store the API key's client ID and secret in the deployment's secret store as `CANVAS_API_CLIENT_ID` and `CANVAS_API_CLIENT_SECRET`.
  </Step>

  <Step title="Create the LTI 1.3 Developer Key">
    Return to the root account's **Developer Keys**, create an **LTI Key**, and choose Canvas's **JSON configuration URL** option. Enter:

    ```text theme={null}
    ${TOOL_URL}/lti/config
    ```

    The configuration document supplies the title, course-navigation and root-account-navigation placements, OIDC initiation URL, target link URI, public JWKS URL, and signed custom fields. Prefer this URL over manually copying fields — the deployed service remains the registration source of truth.

    For reference, the individual values the configuration document provides are:

    | Canvas field           | Value                               |
    | ---------------------- | ----------------------------------- |
    | JSON configuration URL | `${TOOL_URL}/lti/config`            |
    | OIDC initiation URL    | `${TOOL_URL}/lti/login`             |
    | Target link URI        | `${TOOL_URL}/lti/launch`            |
    | Redirect URI           | `${TOOL_URL}/lti/launch`            |
    | Public JWK URL         | `${TOOL_URL}/.well-known/jwks.json` |

    Enable the key and record its client ID as `LTI_CLIENT_ID`.
  </Step>

  <Step title="Install the External App">
    At the root account (or desired account scope), open **Settings → Apps → View App Configurations**. Add an app **By Client ID**, paste the LTI client ID from the previous step, approve the registration, and record the deployment ID Canvas assigns as `LTI_DEPLOYMENT_ID`.

    Set the deployment ID in `LTI_DEPLOYMENT_ID`, update the LTI client ID in the deployment's secret store, and deploy a new service revision before testing. On Cloud Run, add numbered Secret Manager versions and re-run the setup or upgrade. On Docker/VPS, update the protected environment or mounted secret and recreate the application container.

    <Note>
      For a controlled self-service rollout where instructors may add the app to their own courses, set `LTI_DEPLOYMENT_ID_CHECKING_ENABLED=false` on the service. This accepts any non-empty deployment ID in a Canvas-signed launch from the configured issuer and client ID. Only use it if everyone able to install this client ID in Canvas is trusted to grant access to the tool.
    </Note>

    Use a root-account-level installation for a broad rollout and for the administrator dashboard. Use a course-level installation only for an isolated instructor/student pilot; a course-level installation does not provide the root-account navigation surface.
  </Step>

  <Step title="Load the Detector Script Through the Canvas Theme">
    The detector runs on Canvas quiz-taking pages. It treats a Canvas access-code field only as a challenge signal, verifies the exact course/assessment against the service, fills an access code only after Config Key proof, shows approved web tools, and detects Canvas-confirmed completion.

    For a Cloud Run bundle deployment, generate the upload-ready loader file first:

    ```bash theme={null}
    ./canvas-theme-loader.sh cloudrun.env
    ```

    This writes a `canvas-theme-loader.js` file containing the exact configured public origin. Upload that generated file rather than manually transcribing the URL.

    The stable loader URL is:

    ```text theme={null}
    ${TOOL_URL}/js/canvas-seb-detector.js
    ```

    From **Admin**, select the intended account, open **Themes**, edit or create the active theme, and upload the loader as its desktop JavaScript. Preview, save, and apply the theme.

    <Note>
      Some self-hosted Canvas deployments reject uploaded theme JavaScript with `ActionController::InvalidCrossOriginRequest` (HTTP 422). If you see that error in the browser console, configure the account theme's `js_overrides` value to the externally hosted loader URL instead:

      ```text theme={null}
      ${TOOL_URL}/js/canvas-seb-theme-loader.js
      ```

      Do not disable Canvas CSRF protection globally to work around this. See the Canvas Setup source documentation for a Rails runner workaround for root-account recovery.
    </Note>
  </Step>

  <Step title="Verify the Integration">
    Use separate administrator, instructor, and student accounts.

    1. As a root-account administrator, retrieve `/lti/config`, confirm Canvas has the intended client and deployment IDs, and confirm both the **Safe Online Exam Admin** root-account navigation entry and the course app in a target course.
    2. Open **Safe Online Exam Admin**, complete Canvas authorization, verify school courses load, create and assign a school tool preset, refresh a test course, reveal and rotate a test exit password, and confirm each operation appears in recent administrator activity.
    3. As an instructor, launch the tool from Canvas, complete OAuth, refresh assessments, enable the assigned school tool, create a quiz-only tool, set an exit-password policy, and enable one Classic Quiz and one New Quiz.
    4. In a normal browser, open each assessment page and verify that the theme loaded the detector script without console errors.
    5. As a student, launch the course-navigation tool, complete the one-time Canvas connection, and run the optional setup check.
    6. Hide the course-navigation placement from students, revisit both assessment pages in a normal browser, and confirm **Open Safe Exam Browser** still starts the signed LTI launch.
    7. Download a fresh configuration. On an approved SEB client, verify the configuration opens, reaches Canvas, proves its Config Key, fills the access-code prompt, and makes only selected exam tools available.
    8. For each assessment type, cancel one Canvas submission confirmation and ensure no exit occurs. Then submit successfully and ensure the exit flow begins only after Canvas shows the authoritative completed state.
  </Step>
</Steps>

## Deployment Boundary

### Why Each Canvas Instance Needs Its Own Deployment

The service has one active LTI platform configuration: `CANVAS_DOMAIN`, `LTI_ISSUER`, `LTI_KEY_SET_URL`, `LTI_AUTH_URL`, `LTI_CLIENT_ID`, and `LTI_DEPLOYMENT_ID`. Canvas cloud values are the defaults. If a service configured with cloud defaults is registered in a self-hosted Canvas, `/lti/login` redirects the embedded tool frame to `sso.canvaslms.com` — a page that is intentionally not embeddable by a self-hosted Canvas — and the browser shows `sso.canvaslms.com refused to connect`.

Deploy a separate service for each independent Canvas instance. Keep separate client IDs, deployment IDs, OAuth credentials, PostgreSQL databases, secrets, and service URLs for environments that must remain isolated (production, test, beta).

### Self-Hosted Canvas Signing Keys

<Warning>
  Canvas must sign LTI launch tokens with an RSA key of at least 2048 bits. Before registering the tool, inspect `${CANVAS_DOMAIN}/api/lti/security/jwks`. Do not use the legacy sample JWKs shipped with some Canvas Docker configurations. The service rejects undersized `RS256` keys rather than accepting a weaker platform signature.

  For a running Canvas instance, rotate the three platform keys through Canvas's own `Lti::KeyStorage` and restart its web service so its JWKS response reloads the new keys. After the restart, verify that every key at `${CANVAS_DOMAIN}/api/lti/security/jwks` has a 2048-bit (or larger) modulus before reopening the external tool.
</Warning>

## LTI Launch Recovery

Use the visible error state to choose the next diagnostic step:

| What appears in Canvas                 | Meaning                                                                              | Recovery                                                                                                                                          |
| -------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sso.canvaslms.com refused to connect` | The tool is using Canvas cloud's authorization endpoint for a self-hosted Canvas.    | Set `LTI_AUTH_URL` and `LTI_KEY_SET_URL` to the self-hosted Canvas endpoints, deploy the tool, and relaunch.                                      |
| `Canvas Signing-Key Error`             | Canvas's platform JWKS contains an RSA key smaller than 2048 bits.                   | Rotate the three `Lti::KeyStorage` keys, restart Canvas web, verify the public JWKS, and reopen the tool.                                         |
| `Invalid LTI Launch`                   | The tool could not verify the signed launch for another reason.                      | Reopen the tool. If it repeats, compare the deployment ID, issuer, client ID, target-link URI, and public JWKS URL with the Canvas Developer Key. |
| `Connect Canvas`                       | The LTI launch succeeded but the current user has not yet granted Canvas API access. | Select **Connect Canvas** and complete the Canvas authorization flow.                                                                             |

## Common Integration Failures

<AccordionGroup>
  <Accordion title="LTI Deployment Configuration Required">
    Deployment-ID checking is enabled and Canvas installed the app with an ID not yet allowed by the service. Retrieve the `deployment_id` from Canvas's External Tools API for that course or account app, append it to `LTI_DEPLOYMENT_ID` without replacing existing IDs, deploy a new revision, then reopen the tool. For an intentional self-service rollout, an administrator can instead set `LTI_DEPLOYMENT_ID_CHECKING_ENABLED=false`.
  </Accordion>

  <Accordion title="Canvas reports an LTI configuration or identity error">
    Refresh the registration from `/lti/config`. Confirm the launch includes the signed numeric Canvas user custom field and that `LTI_DEPLOYMENT_ID` matches the installed app.
  </Accordion>

  <Accordion title="Instructor is asked to authorize repeatedly">
    Confirm the API OAuth redirect URI exactly matches `${TOOL_URL}/api/oauth2callback`, the OAuth key is enabled, and the configured client ID and secret are the API-key values (not the LTI key values).
  </Accordion>

  <Accordion title="Administrator dashboard is missing or denied">
    Install the LTI app at the root account from the current `/lti/config`. Use an actual root-account administrator. Confirm Canvas expands the account/root-account/root-admin custom fields and that the administrator OAuth scopes are allowed. A course installation or instructor enrollment is intentionally insufficient.
  </Accordion>

  <Accordion title="Student cannot connect Canvas or configuration download fails">
    Confirm the exact `url:GET|/api/v1/login/session_token` scope is allowed in the OAuth key and that the student is authorizing the same Canvas environment as the LTI launch.
  </Accordion>

  <Accordion title="Access code is not filled in SEB">
    Confirm a fresh configuration was downloaded, the detector loaded on the actual assessment route, Config Key proof succeeded, and the Canvas prompt is not ambiguous.
  </Accordion>

  <Accordion title="Detector never loads">
    Check the active account theme, inherited theme behavior, browser console, CSP headers, and that the public detector URL returns JavaScript content.
  </Accordion>
</AccordionGroup>

## Next Steps

With Canvas registered and the detector loaded, provision the SEB client encryption identity following the [Certificate Management](/deployment/certificate-management) guide, then complete the full Canvas and SEB acceptance sequence described in the [Testing](/operations/testing) guide.
