Skip to main content
Safe Online Exam exposes a set of public and LTI 1.3 routes that Canvas and browsers use during tool discovery, OIDC login initiation, and signed launch validation. This page documents every route in the Public and LTI section of the route reference, including the parameters each endpoint validates, who calls it, and what authentication is required.
Routes marked with a compatibility contractGET /lti/config, GET|POST /lti/login, GET|POST /lti/launch, and GET /.well-known/jwks.json — are stable public surfaces. Canvas registrations and SEB clients depend on them remaining at the same paths across upgrades.

Service Status and Canvas Fallback

GET /

Returns the Safe Online Exam service-status shell page. In normal operation this route is the application root; it renders a lightweight React view that includes optional testbed diagnostics when the testbed feature is enabled. Caller: Browser, operator, health monitoring. Auth: None. Response: 200 text/html — the React app shell with view service-status.

GET /login

Returns a plain HTML fallback page that instructs users to launch the tool from Canvas. Canvas may redirect unauthenticated users here when an existing session cookie has expired or when a browser navigates directly to the tool URL. Caller: Browser (direct navigation or Canvas redirect). Auth: None. Response: 200 text/html — static fallback instructing the user to open Canvas.

Health Endpoints

The service exposes three independent health routes covering the HTTP layer, the login path, and the static JavaScript delivery path. All three return the same lightweight JSON body.
Purpose: Primary liveness check. Used by load balancers, Cloud Run, and Docker Compose health checks.Caller: Infrastructure health probes.Auth: None.Response:
Use GET /ready (not GET /health) for readiness checks that must confirm database migrations are applied. /ready returns 503 if any checked-in migration has not been run.

Setup Handoff Pages

GET /setup and GET /setup/guide

Render a role-oriented setup guide for Canvas administrators installing the tool. Both routes return the React app shell with view admin-setup. The /setup/guide variant enables a more detailed walkthrough mode (detailed: true); /setup shows the summary view. The app shell includes the service’s current toolUrl, healthUrl, ltiConfigUrl, jwksUrl, and detectorUrl values so that the person reading the page can copy the correct endpoint URLs into Canvas without referencing separate documentation. Caller: Browser (Canvas or service administrator during initial configuration). Auth: None. Response: 200 text/html — React app shell with view admin-setup.

LTI Signing Keys

GET /.well-known/jwks.json

Returns the service’s active RSA public key set in JSON Web Key Set format. Canvas fetches this endpoint when validating the tool’s registration and when verifying any signed assertion the service issues during the OIDC flow. Caller: Canvas LTI platform (during developer key registration and on-demand key refresh). Auth: None — this endpoint is intentionally public. Compatibility contract: This path must remain stable. Canvas caches the JWKS URL from the tool’s developer key registration; changing the path requires updating every Canvas installation. Response:
array
required
Array of public JWK objects. The service only publishes RSA keys. Each key has at minimum kty, alg, use, kid, n, and e fields. The private exponent d is never included.
The service enforces a minimum RSA modulus of 2048 bits at startup. If Canvas presents a platform signing key smaller than 2048 bits during a launch, the token validation stage rejects it and returns an error page explaining the requirement to the user.

LTI Configuration Document

GET /lti/config

Returns the Canvas LTI 1.3 JSON configuration document that a Canvas administrator uses to register or update the tool’s developer key. The document describes both the course navigation placement (for instructors and students) and the account navigation placement (for root-account administrators). Caller: Canvas administrator during developer key setup, or automated registration tooling. Auth: None — this document contains only public tool metadata. Compatibility contract: This path must remain stable. Response: 200 application/json
The visibility of the course navigation placement is controlled by the LTI_COURSE_NAVIGATION_VISIBLE_TO_STUDENTS configuration flag. When set to false, the placement is visible only to Canvas admins and teachers; when unset or true, it is visible to all course members. Custom fields requested per placement:
A Canvas administrator must refresh the LTI Developer Key configuration from this endpoint any time the tool URL changes or a new Canvas deployment is added to the service. Stale registrations will produce CanvasLtiConfigurationError at launch time if required signed substitution fields are absent.

OIDC Initiation

GET /lti/login and POST /lti/login

Handles the LTI 1.3 OIDC third-party initiation request. Canvas sends this request before the signed launch to establish a nonce and state token that the service will later verify at /lti/launch. Both GET and POST methods are accepted; the service normalizes query parameters and body fields identically. Caller: Canvas LTI platform — the first step in every LTI launch. Auth: None at this stage (the request is from Canvas, not a logged-in user). The service validates issuer and client ID before creating state. Compatibility contract: This path must remain stable. Rate limiting: Process-local and distributed IP-based admission budgets apply. Exceeding the budget returns 429 with a retry-after: 60 header. Request parameters (query string for GET, form body for POST):
string
required
Canvas issuer URL. Must exactly match the configured LTI_ISSUER value (e.g., https://canvas.instructure.com).
string
required
Opaque Canvas user hint used to pre-select the user in Canvas’s authorization step. Passed through unchanged to Canvas.
The URI Canvas wants the service to launch. Must match the tool origin and be either /lti/launch or a /seb/launch/:contentId path. Maximum 2048 characters.
string
Optional Canvas LTI client ID. When present, must match LTI_CLIENT_ID.
string
Optional Canvas deployment ID. When deployment ID checking is enabled (LTI_DEPLOYMENT_ID_CHECKING_ENABLED is not false), this value must appear in the LTI_DEPLOYMENT_ID allowlist. When absent and checking is enabled, the login is rejected.
string
Optional Canvas message hint forwarded unchanged to the Canvas authorization URL. Maximum 8192 characters.
On success: The service:
  1. Validates envelope size bounds on all parameters.
  2. Verifies iss, optional client_id, target_link_uri, and optional lti_deployment_id.
  3. Generates a cryptographic nonce (randomUUID()).
  4. Creates a browser transaction cookie: a short-lived HttpOnly secure cookie containing a transaction ID and binding hash, scoped to the launch lifetime.
  5. Encrypts and stores state (nonce, target URI, issuer, transaction binding, optional deployment ID) in PostgreSQL with a 10-minute TTL.
  6. Redirects 302 to Canvas’s OIDC authorization endpoint with:
    • scope=openid
    • response_type=id_token
    • response_mode=form_post
    • prompt=none
    • redirect_uri=<toolUrl>/lti/launch
    • nonce, state, login_hint, and any forwarded hints
Error responses:

Signed LTI Launch

POST /lti/launch

Receives and validates the Canvas-signed LTI ID token after the OIDC authorization step. This is the most security-critical endpoint in the service; it performs the complete validation chain before creating a verified LTI principal in the session. Caller: Canvas LTI platform — Canvas POSTs an HTML form to this URL as the final step of every LTI launch. Auth: Canvas RS256-signed ID token (validated by the service against the configured Canvas JWKS). Compatibility contract: This path must remain stable. Rate limiting: Process-local and distributed IP-based budgets apply independently from the login budget. Request body (form POST):
string
required
The RS256-signed LTI ID token issued by Canvas. The service validates signing, issuer, audience, nonce, token age, timestamps, LTI version and message type, deployment ID, target link URI, and the OIDC state tuple.
string
required
The encrypted state token created during /lti/login. Used to look up the stored nonce, target URI, and browser transaction binding.
string
When Canvas itself encounters an error, it POSTs an error field instead of an ID token. The service returns 400 with an explanatory HTML page.
Validation chain (executed in order):
1

Envelope check

Verifies that state and id_token meet bounded-size requirements before any cryptographic work.
2

Rate admission

Consumes process-local and distributed IP admission budgets. Returns 429 if exhausted.
3

State peek

Decrypts the state token from PostgreSQL to retrieve the stored nonce and browser transaction binding. The state record has not been consumed yet.
4

Browser transaction cookie

Verifies that the browser’s transaction cookie matches the binding hash stored in state. Fails if the cookie is absent (e.g., blocked third-party cookies) or does not match.
5

Distributed state admission

Consumes a per-state distributed admission budget before proceeding to token validation, preventing distributed brute-force attempts on a single state token.
6

Token validation

Validates the ID token RS256 signature against the Canvas JWKS, then checks issuer, audience, nonce, token age, issued/expiry timestamps, LTI version, message type, and deployment ID.
7

Login–launch binding

Confirms that the token’s issuer, deployment ID, and target link URI match the values stored in the OIDC state. Prevents state substitution between logins.
8

Atomic state claim

Atomically marks the state token as consumed in PostgreSQL. Prevents replay of the same state/token pair from a second browser.
9

Session regeneration

Calls Express regenerateSession to issue a fresh session ID before writing any principal, preventing session fixation.
10

Principal creation

Creates a VerifiedLtiPrincipal from the validated launch data — either a course principal (instructor/student) or an account admin principal — and writes it to the new session.
After validation — role routing: Error responses:

GET /lti/launch

Handles same-origin browser reloads and redirects within an active verified session. Canvas never calls this route directly — it is used by the React client when the user navigates within the already-authenticated tool (e.g., after Canvas OAuth completes and the tool redirects back with ?connected=1). Caller: Browser (same-origin navigation within an active tool session). Auth: Existing verified LTI principal in the Express session. Returns 302 /login if no principal is present. Fetch Metadata guard: Requests where sec-fetch-dest is iframe and sec-fetch-site is cross-site are rejected with 403. This prevents Canvas from re-entering a live session through a cross-site iframe navigation. Query parameters:
string
Optional. When present, must exactly match principal.courseId. Returns 403 on mismatch.
string
Optional. When present, must exactly match principal.canvasUserId. Returns 403 on mismatch.
string
Optional. Signal from the OAuth callback that Canvas authorization just completed. Not validated; the role-routing logic re-reads the principal from session.
Response: Same role-routing logic as POST /lti/launch but sources the principal entirely from the existing session rather than from a new token.