Skip to main content
Safe Online Exam’s architecture page describes the implementation boundaries that matter to operators, security reviewers, and contributors. It covers the system’s four core responsibilities, the runtime component graph, code ownership by area, how identity and authorization are established through LTI launches and Canvas OAuth, the assessment and course model, the full SEB configuration lifecycle from instructor setup to student exit, the configuration security boundary and certificate encryption model, and the ten PostgreSQL tables with their expiration behavior. For deployment commands, see Deployment Overview. For Canvas registration steps, see Canvas Setup.

System Purpose

Safe Online Exam is an LTI 1.3 tool for requiring Safe Exam Browser on Canvas Classic Quizzes and New Quizzes. It has four connected responsibilities:
  1. Give instructors a course-scoped interface for discovering Canvas assessments and managing SEB policy.
  2. Give verified root-account administrators a Canvas-embedded, school-wide recovery interface with controlled password reveal, active-course connection, and bulk tool rollout.
  3. Generate protected SEB configurations and establish a Canvas session inside SEB without transferring a normal-browser session cookie.
  4. Release the Canvas access code and approved web-tool capability only when SEB proves that it is using the current configuration.
The service is not a general Canvas proxy. A deployment is configured for one Canvas origin and one LTI deployment boundary. All identity and authorization data used for a request comes from a validated LTI launch or a server-issued, bound capability.

Runtime Shape

The application is one Node.js process. NestJS controllers expose HTTP endpoints, services own protocol and business behavior, repositories provide PostgreSQL storage in deployed environments and in-memory storage for local or test work, and React renders page views supplied by the server app shell. The detector is a separately served browser asset that runs on Canvas quiz pages.
The production image has no runtime dependency on a Google Cloud SDK. Durable state is in PostgreSQL, application configuration is read from environment variables or mounted files, and the HTTP process listens on a configurable host and port. Multiple instances share sessions, one-time claims, admission budgets, and operation locks through PostgreSQL, so sticky sessions and a writable application filesystem are not required. Cloud Run, Cloud SQL, Artifact Registry, Secret Manager, and Cloud Build are the recommended managed deployment, not application requirements. Another platform can run the same image when it provides PostgreSQL 17+, public HTTPS ingress, secret injection, an exact-image migration job before traffic, and a scheduled cleanup job. See Deployment Overview for both supported operating models.

Code Ownership

The client router loads the React view module selected by the server bootstrap instead of shipping every administrator, instructor, student, and SEB workflow in the entry bundle. Route chunks and the shared React runtime use content-hashed filenames with immutable caching. The app shell reads Vite’s build manifest to preload the shared runtime without adding a first-load request waterfall.

Identity and Authorization

LTI Launch Validation

Canvas initiates OIDC at /lti/login. The service verifies the issuer, requested target link URI, configured client ID, and configured deployment ID before creating encrypted state. State is valid for ten minutes and is additionally bound to a short-lived, HttpOnly, secure browser transaction cookie. Canvas posts an ID token to /lti/launch. The service validates:
  • RS256 signing against the configured Canvas JWKS.
  • Issuer, audience, nonce, token age, issued/expiry timestamps, LTI version, message type, and deployment ID.
  • The target link URI and initiation state tuple.
  • The initiating browser transaction cookie.
  • A durable, atomic PostgreSQL state claim to prevent replay.
After successful validation, the server regenerates the Express session and stores a verified LTI principal. A course principal contains the signed issuer, deployment, subject, numeric Canvas user ID, course ID, roles, and custom fields. A root-account administrator principal instead contains signed numeric account/root-account IDs and Canvas’s root-admin substitution. Query parameters and request bodies cannot substitute for either principal.
Instructors receive the course management view. Students receive a launch-only flow and never receive management actions. The school dashboard is available only through the root-account navigation placement and requires both a standard signed LTI Administrator role and Canvas’s signed root-account-admin value. A Canvas administrator must refresh the LTI registration if Canvas does not supply the configured substitutions.

Canvas OAuth Flow

An LTI launch authenticates a person but does not authorize Canvas API calls. The application uses a separate Canvas OAuth authorization for API access:
1

Initiate from a verified launch

An instructor opens /api/oauth2authorize or /api/oauth2reauthorize from the same-origin tool UI created by an existing verified launch. Browser requests whose Fetch Metadata identifies any other site relationship are rejected.
2

Record state and redirect

The service records encrypted, one-time state and redirects to Canvas.
3

Exchange the authorization code

/api/oauth2callback verifies state and exchanges the authorization code for access and refresh tokens.
4

Persist one grant per Canvas user

PostgreSQL stores one OAuth grant per Canvas user ID. Administrator authorization upgrades that same record to the complete application-plus-administrator scope set. CanvasApiService refreshes it when necessary.
5

Complete in a popup or return through Canvas

Instructor and administrator authorization normally runs in a popup while the signed LTI page remains open. The callback renders a non-privileged completion screen that sends a fixed completion message only to its exact same-origin opener; the opener verifies both the message origin and popup window before performing its own same-origin refresh. If a popup is unavailable, the user returns through Canvas. An OAuth callback never renders or directly redirects into an authenticated management view.
Every Canvas OAuth connection requests the same complete application scope set, including the course-list permission used for teacher-scoped tool duplication and the session-token permission required for SEB handoff. This is an explicit product decision: one durable grant must remain valid when a person is an instructor in one course and a student in another. Canvas still enforces the user’s actual course permissions; the application never treats scope possession as an LTI role or course-authorization check.
Access and refresh tokens are encrypted before PostgreSQL persistence with an independent AES-256-GCM keyring. The record ID and Canvas user ID are authenticated as associated data, while non-secret scope and identity metadata remains queryable. Legacy plaintext rows are read-only to support a staged migration and are rewritten by the explicit maintenance command.
Root-account administrators use /api/admin/oauth2authorize to upgrade their existing user grant with administrator scopes. Every admin mutation also requires a short-lived HMAC action token bound to the LTI subject, Canvas user, root account, deployment, and current Express session.

Assessment and Course Model

Identifiers

All persisted assessment records use a canonical public content ID: The assessments table stores Canvas discovery data, availability verification, and SEB state. courses stores course-level defaults and its exam-tool catalog. Course defaults can provide URL policy, start/exit password policy, and selected exam tools; an assessment may inherit defaults, retain an explicit list of course tool IDs, and add quiz-only tool definitions.

Canvas Discovery and Availability

Instructor discovery refreshes Classic and New Quiz data from Canvas. A learner can use an assessment only when its cached Canvas verification is current, explicitly verified, published, and within its global unlock/lock window. The verification window is 24 hours. Missing records are retained for instructor reconciliation but fail closed for learners; a failed refresh marks the cached discovery stale. Assessment updates use short-lived PostgreSQL operation locks while Canvas and database state are changed. Administrator course resets take a course-level lease and then the same per-assessment leases, so ordinary assessment mutations cannot overlap a reset. Course refreshes, administrator connection-count writes, school-preset assignment writes, and instructor tool copies use the same course fence so they cannot recreate state after a reset. Atomic compare-and-delete/insert operations prevent overlapping workers from owning the same lease and help keep Canvas access codes aligned with persisted SEB settings. Each workflow carries a shared lease guard across nested locks and checks it after external reads and before later Canvas or PostgreSQL mutations; a failed background renewal makes the guard reject those later side effects immediately. After final ownership verification, releasing a lease is best-effort: a cleanup failure cannot replace the verified action result, and the bounded lease expires automatically. An administrator course reset performs read-only, strict Classic Quiz and New Quiz discovery and reads every assessment’s current Canvas access-code state before making the first destructive call. The discovery result is assembled in memory instead of changing cached learner-verification state that would need recovery if the preflight aborts. The reset then removes each Canvas access code with the account-administrator grant and only afterward deletes course-related transient state, assessments, the course policy, and per-course school-tool preset assignments in one PostgreSQL transaction. That transaction also stores an operation-specific reset receipt on the retained root-account course connection; the shared OAuth grant is deliberately retained. If a Canvas response is lost, a later Canvas mutation fails, or the database transaction definitively fails, the service restores the exact pre-reset Canvas state for every assessment that may have changed and restores its prior local assessment record in reverse order. If the transaction commit response is ambiguous, the service verifies the durable reset receipt before compensating. An unavailable receipt check, failed compensation, or lost lease is reported as an indeterminate result that requires refresh and verification rather than blindly restoring Canvas state.

Exam Tools and URL Policy

Course-owned exam tools have an exact HTTPS launch URL and typed resource rules: one exact page or file, an address and related links, or an explicitly confirmed whole website. Instructors explicitly approve both tool start pages and resources, including a different HTTPS website such as a CDN asset; the instructor UI calls out cross-site access before saving. A saved instructor-owned tool can be duplicated into active Canvas courses where the same OAuth user is a teacher. The browser can only choose from a Canvas-filtered course list, and the server retrieves that list again before every target write; target IDs alone never authorize a copy. The server also snapshots each target’s durable reset generation before those external reads and rejects the copy if a reset completed in the meantime or if reset-deleted course setup has not yet been recreated by a teacher launch. The copy appends a local tool without replacing the target catalog, preserves an existing equivalent definition on retry, and propagates the target course defaults so relevant configuration fingerprints are invalidated. A dedicated YouTube video tool accepts a watch, share, Shorts, or embed link and turns it into one embedded public video with a server-owned player page and bounded media policy. The server-owned page supplies YouTube’s required embedding identity while deliberately excluding YouTube browsing and Google sign-in. User-entered general rules remain restricted to safe HTTPS URLs or concrete domains; wildcards, credentials, arbitrary regular expressions, and unsafe historical patterns are rejected or quarantined. Root administrators can create reusable school presets in admin_tool_presets and assign them to individual courses. An assigned definition is synchronized into the course catalog as school-managed: instructors may enable or disable it but cannot silently change its launch URL or resource access. Updating or deleting the preset synchronizes every assigned course and invalidates affected configuration fingerprints. Quiz-only definitions remain on the assessment record and never become course defaults.
The SEB URL filter in the generated configuration is the control that determines what can load. Changing any selected tool or URL policy changes the configuration fingerprint; students must download a new configuration.

SEB Lifecycle

Instructor Configuration

When an instructor enables SEB, AssessmentService creates an access code, mutates the appropriate Canvas assessment, and persists SEB state only after the mutation is successful. Enabling requires an effective exit password: assessment override, course default, or configured managed default. Optional start passwords protect the inner configuration payload. Password responses are redacted by default; an instructor can make a narrowly bound, short-lived reveal request.

Student Configuration Download

1

Request a configuration grant

A verified LTI principal calls the grant endpoint. The server mints a one-time, 120-second capability bound to the principal, course, content ID, and current settings fingerprint.
2

Obtain a fresh Canvas session URL

For the download, the service obtains a fresh Canvas session URL server-side. Browser cookies are never copied to the configuration or exposed through the API.
3

Build and encrypt the .seb file

The service builds the SEB start URL around the Canvas session URL, assembles the configuration (assessment entry route, URL policy, Config Key behavior, HMAC-bound quit URL), and encrypts it to the configured public certificate. The service holds only public encryption material.
4

Deliver on grant consumption

GET /seb/config/:courseId/:contentId.seb consumes the one-time capability and streams the encrypted configuration to the student’s browser.

Config Key Proof and Access-Code Release

1

Detector reads the Config Key

On the Canvas assessment page, the detector reads the SEB Config Key hash and current browser URL through the SEB JavaScript API.
2

Request a proof token

The detector posts to POST /api/seb/access-proof/:courseId/:quizId. The server verifies the assessment, current settings fingerprint, URL family, and Config Key hash before returning a one-time proof token valid for two minutes.
3

Redeem the proof for the access code

POST /api/seb/access-code/:courseId/:quizId consumes the proof token and returns the access code, approved tools, and an exit grant with sensitive response headers. A proof can be consumed exactly once.
4

Fill the Canvas access-code prompt

The detector fills only an unambiguous Canvas access-code prompt. It does not treat DOM content as authorization.
In an ordinary browser, the detector calls GET /api/seb/requirement/:courseId/:quizId before showing the SEB-required prompt. That endpoint performs one exact assessments primary-key lookup and returns sebRequired: true only when the stored course/content relationship matches and the SEB configuration is enabled, required, and usable. An absent, mismatched, disabled, malformed, or unverifiable result does not produce a launch prompt.

Completion and Exit

The detector waits for Canvas-authored completion evidence. Classic Quiz completion requires a successful final submission and the matching Canvas result structure; New Quiz completion requires the authoritative result UI. On confirmed completion, the detector uses a settings-bound exit grant to display a quit link. Unbound manual and automatic quit paths intentionally return 410 rather than accepting a general-purpose quit request.

Setup Check

/seb/check/config.seb generates a separate configuration for testing certificate decryption, SEB runtime detection, connectivity, storage, and Config Key proof. It never releases an assessment access code and does not establish device trust. It should be part of pre-exam readiness testing, not a substitute for device management.

Configuration Security Boundary

Generated assessment configurations include a strict Canvas and approved-resource URL filter, SEB Config Key proof setup, session-monitoring and kiosk-related policy, exit protection, and optional start-password protection. The exact plist is built by SebConfigurationService.
Use integration testing with the supported SEB clients rather than assuming a setting is honored by every client release or operating system. Validate the complete policy with a real supported client after SEB or operating-system updates.

Certificate Encryption Model

Certificate encryption is enabled by default, including in hardened runtimes. The public X.509 certificate or public key permits wrapping the file; the matching private identity belongs only on approved client devices. An instance that cannot distribute that identity may explicitly set SEB_CONFIG_ENCRYPTION_ENABLED=false; in that mode the configuration is plaintext unless an instructor sets a start password. Assessment configurations use the SEB exam-start purpose and include the assessment start URL, an HMAC-bound quit URL, URL filter rules, a derived Browser Exam Key, and a Config Key salt. If an instructor sets a start password, the inner configuration is password-protected before certificate wrapping. The outer encrypted file uses SEB’s public-key-hash (pkhs) format when a public certificate is configured. The setup-check configuration is deliberately different from an assessment configuration: it starts at /seb/check, has no assessment access code, allows quit without an assessment exit password, and cannot redeem an access-code proof.
On macOS, the generated policy requires Automatic Assessment Configuration (AAC) through both enableMacOSAAC and lockdownModePolicy, requires installation from the system Applications location, and sets a macOS 12.1 floor through explicit version-number settings plus the coarse version field. The overlapping AAC keys cover supported SEB client generations; they do not replace device management. AAC may block third-party assistive technology, so an accommodation that requires it needs a separate approved assessment or proctoring arrangement rather than a weakened common configuration.
On Windows, the configuration requests the OS-session and SEB-service controls used by the generated policy, including the kiosk desktop, process/session monitoring, and the supported SEB version floor. Client releases that do not understand a newer configuration key cannot enforce that key, so managed-device policy must also pin the approved SEB client version and integrity baseline.
Assessment lockdown settings explicitly block configuration surfaces that would undermine the browser boundary: application/user switching, virtual machines, additional displays, screen capture/sharing, AirPlay, Siri and dictation, developer console, printing, downloads/uploads, open/save panels, and non-SEB clipboard transfer. They leave Canvas-required browser behavior, reload, JavaScript, and SEB-managed browser windows available so Canvas and approved web tools can function.

Persistence and Expiration

PostgreSQL transactions and row locks implement atomic state claims, one-time token consumption, rate-budget increments, and operation-lock ownership. Claim/consume operations use conditional mutations so two app instances cannot both win the same one-time claim. Cleanup selects bounded batches with FOR UPDATE SKIP LOCKED, allowing safe overlap without long table locks. This is why multiple app instances can share runtime state without sticky sessions. Schema changes are explicit, ordered migrations recorded with checksums in schema_migrations. The application never mutates schema on ordinary startup; /ready fails until all checked-in migrations are applied. A migration job must complete before traffic reaches a new image.

HTTP and Security Controls

  • Security headers are applied before application routes; Express disables x-powered-by and trusts one proxy hop.
  • Sessions use HttpOnly cookies and use Secure; SameSite=None when the configured tool URL is HTTPS or the profile is production.
  • Sensitive proof, access-code, and password-reveal responses are no-store and are bound to the verified session/principal.
  • LTI initiation and token validation use process-local and PostgreSQL-backed admission budgets. Configuration-grant minting is rate-limited per principal and IP.
  • Canvas API calls are constrained to the configured Canvas origin and /api/v1 base. Responses have size limits and upstream deadlines; a 401 triggers at most one safe token refresh/retry.
  • The public detector script has two stable paths. Debug or diagnostic modes serve a readable, non-cacheable asset; normal production mode serves the built minified asset from the same public path.
  • Public JavaScript and CSS responses are compressed; user-specific HTML and API responses remain outside that compression middleware.
The detector’s buttons and sidebar are user-interface affordances. The generated SEB URL filter, current configuration fingerprint, server-side proof, and Canvas-authored completion state are the enforcement boundaries. Changing any selected tool or URL policy changes the configuration fingerprint; students must download a new configuration.

Route Reference

The application exposes six groups of public HTTP routes: LTI and health routes, Canvas OAuth routes, SEB student and configuration routes, the Canvas detector and diagnostics routes, instructor assessment routes, and root-account administrator routes. The route handlers are the source of truth for parameters and response schemas; unlisted query parameters and output fields are implementation details.

LTI Routes

OIDC initiation, signed LTI launch handling, Canvas JSON configuration, JWKS, and health endpoints.

OAuth Routes

Canvas OAuth authorization, callback, status, and student-session authorization endpoints.

SEB Student Routes

Configuration grant minting, encrypted .seb download, Config Key proof, access-code redemption, setup check, and exit flows.

Detector Routes

Stable detector script delivery, Canvas theme loader, compatibility alias, and diagnostic trace endpoint.

Instructor Routes

Course assessment discovery, SEB settings, enable/disable, refresh, defaults, and password reveal.

Admin Routes

Root-account course connections, resets, password reveal, term selection, and school tool preset management.