> ## 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.

# SEB Student Config Grant, Access-Code, and Exit Routes

> One-time grants, encrypted .seb download, Config Key proof, access-code release, session readiness, launch handoff, and exit flows for Safe Online Exam.

Safe Online Exam exposes a layered set of student-facing routes that carry a student from a signed Canvas LTI launch through encrypted SEB configuration download, Config Key proof, access-code release, and finally a settings-bound exit. This page documents every endpoint involved in that flow — configuration grants, `.seb` file delivery, public encryption certificate access, the proof/redemption cycle, session readiness, setup-check, assessment launch handoff, and the exit surface — together with the security constraints the server enforces at each step.

## Content ID Formats

Every student route that accepts a `contentId` or `quizId` parameter expects one of two canonical formats. The server normalises incoming values to these forms before any database lookup.

| Assessment type | Canonical content ID format         |
| --------------- | ----------------------------------- |
| Classic Quiz    | `classicquiz_{quizId}`              |
| New Quiz        | `newquiz:{courseId}:{assignmentId}` |

Identifiers outside these patterns return `403` or `404` without a lookup. Both formats are listed in the Compatibility Contracts section of the README and are treated as stable public identifiers.

***

## Configuration Download Flow

Students do not use a static or reusable `.seb` link. The server issues a one-time, short-lived capability grant from a verified LTI session. The SEB client then presents that grant token as a query parameter when downloading the encrypted file.

### POST /api/seb/config-grant/:courseId/:contentId

Mints a one-time configuration download grant bound to the verified LTI principal, course, content ID, and current settings fingerprint.

<Note>
  The grant expires after **120 seconds** and can be consumed exactly once. A `HEAD` request from Windows SEB validates the grant URL without consuming it, so the single `GET` download retains its one-time claim.
</Note>

**Authentication:** Requires an active verified LTI principal for the given `courseId`. The request must also carry a valid SEB config-grant action token in the request body and pass Fetch Metadata integrity checks. Students who require a Canvas session handoff must have an authorised Canvas OAuth grant before a configuration grant is issued.

**Path parameters**

<ParamField path="courseId" type="string" required>
  Canvas course ID. Must match the `courseId` on the verified LTI principal stored in the Express session.
</ParamField>

<ParamField path="contentId" type="string" required>
  Canonical content ID: `classicquiz_{quizId}` for Classic Quizzes or `newquiz:{courseId}:{assignmentId}` for New Quizzes.
</ParamField>

**Request body**

<ParamField body="handoffPurpose" type="string">
  Optional launch purpose override. Accepts `"student-list"` to change the browser handoff label; any other value defaults to `"assessment"`.
</ParamField>

**Success response — 200**

```json theme={null}
{
  "success": true,
  "sebLaunchUrl": "sebs://example.com/seb/config/12345/classicquiz_67890.seb?grant=<token>",
  "handoffUrl": "/seb/launch-handoff?key=<handoff-token>",
  "expiresInSeconds": 120
}
```

<ResponseField name="sebLaunchUrl" type="string">
  A `sebs://` URL pointing to the `.seb` download endpoint. The browser hands this to the OS to open SEB directly.
</ResponseField>

<ResponseField name="handoffUrl" type="string">
  Path to the browser-side handoff page (`/seb/launch-handoff?key=…`) that redirects the normal browser while SEB opens.
</ResponseField>

<ResponseField name="expiresInSeconds" type="number">
  Token lifetime in seconds. Always `120`.
</ResponseField>

**Error responses**

| Status | `error_code`                            | Meaning                                                                        |
| ------ | --------------------------------------- | ------------------------------------------------------------------------------ |
| `403`  | `LTI_PRINCIPAL_REQUIRED`                | No valid LTI session or course mismatch.                                       |
| `403`  | `CANVAS_SESSION_AUTHORIZATION_REQUIRED` | Student requires Canvas session handoff but OAuth grant is missing or expired. |
| `404`  | —                                       | SEB configuration is unavailable for this assessment.                          |
| `429`  | `RATE_LIMITED`                          | Per-principal or per-IP grant budget exceeded.                                 |

***

### GET /seb/config/:courseId/:contentId.seb

Consumes a configuration grant and streams the encrypted SEB configuration file to the client.

The server validates the grant token, confirms the current settings fingerprint has not changed since the grant was minted, obtains a fresh Canvas session URL for the student, builds the complete SEB plist (assessment start URL, URL filter, Config Key salt, HMAC-bound quit URL), and encrypts it with the configured public certificate before sending the response.

**Authentication:** No session required. The one-time grant token in the `grant` query parameter is the sole credential for this endpoint.

**Path parameters**

<ParamField path="courseId" type="string" required>
  Canvas course ID.
</ParamField>

<ParamField path="contentId" type="string" required>
  Canonical content ID (without the `.seb` extension as a path segment — the `.seb` suffix is part of the route pattern, not a query parameter).
</ParamField>

**Query parameters**

<ParamField query="grant" type="string" required>
  One-time grant token returned by `POST /api/seb/config-grant`. Must be a 43-character base64url string.
</ParamField>

**Success response — 200**

```text theme={null}
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="quiz_<courseId>_<contentId>.seb"
Content-Description: Safe Online Exam Configuration
```

The response body is the binary encrypted SEB configuration file.

**Error responses**

| Status | Meaning                                                                               |
| ------ | ------------------------------------------------------------------------------------- |
| `403`  | Grant is missing, expired, already consumed, or the settings fingerprint has changed. |
| `400`  | Configuration could not be built (instructor should verify quiz settings).            |
| `429`  | Per-process or per-IP download budget exceeded. Retry after 60 seconds.               |

<Note>
  Windows SEB issues a `HEAD` request before downloading. The server validates but does not consume the grant on `HEAD`, preserving the one-time claim for the subsequent `GET`.
</Note>

***

## Public Encryption Certificate Endpoints

These routes serve the public X.509 certificate used to encrypt `.seb` configurations. They carry no authentication requirement and are intended for device administrators who need to distribute the decryption identity.

### GET /seb/config-encryption-certificate.pem

Downloads the active encryption certificate in PEM format.

**Success response — 200**

```text theme={null}
Content-Type: application/x-pem-file
Content-Disposition: attachment; filename="seb-config-encryption-certificate.pem"
x-seb-public-key-hash: <hex-encoded SHA-256 public key hash>
```

**Error response**

| Status | Meaning                                                                                                        |
| ------ | -------------------------------------------------------------------------------------------------------------- |
| `404`  | No encryption certificate is configured (`SEB_CONFIG_ENCRYPTION_ENABLED=false` or certificate not yet loaded). |

***

### GET /seb/config-encryption-certificate.cer

Downloads the same certificate in DER (binary) format, suitable for direct import into Windows Certificate Manager or macOS Keychain.

**Success response — 200**

```text theme={null}
Content-Type: application/pkix-cert
Content-Disposition: attachment; filename="seb-config-encryption-certificate.cer"
x-seb-public-key-hash: <hex-encoded SHA-256 public key hash>
```

***

## Requirement Check

### GET /api/seb/requirement/:courseId/:quizId

Returns whether SEB is currently required for the given assessment. This endpoint is called by the Canvas detector and the Canvas theme loader before loading the full detector script.

**Authentication:** None. This endpoint is public and unauthenticated.

**Path parameters**

<ParamField path="courseId" type="string" required>
  Canvas course ID.
</ParamField>

<ParamField path="quizId" type="string" required>
  Content ID in canonical form.
</ParamField>

**Success response — 200**

```json theme={null}
{ "success": true, "sebRequired": true }
```

or

```json theme={null}
{ "success": true, "sebRequired": false }
```

The response is always `success: true`. A `sebRequired: false` result means SEB enforcement is absent, disabled, misconfigured, or unverifiable — the detector never shows a prompt on `false`.

The server performs one exact primary-key lookup against the `assessments` table and coalesces concurrent identical requests through a short, bounded in-memory promise cache. The cache prevents a burst of simultaneous student page loads from all hitting the database; the result is always private and not cached by the browser (`cache-control: private, no-store`).

***

## Config Key Proof and Access-Code Redemption

The two-step proof/redemption cycle is the central enforcement point. SEB obtains a Config Key hash from its JavaScript API, sends it to the proof endpoint, receives a one-time proof token, and immediately redeems that token for the Canvas access code.

### POST /api/seb/access-proof/:courseId/:quizId

Validates that SEB is running the current configuration for this assessment and mints a one-time proof token.

The server checks that the assessment exists, SEB is enabled and required, the effective exit password is set, and either the `configKeyHash` in the request body matches the expected Config Key for the current settings fingerprint, or the `x-safeexambrowser-configkeyhash` request header carries a valid hash. For session-handoff configurations the server can also validate through the handoff Config Key.

**Authentication:** None (public endpoint). Identity comes entirely from the Config Key hash and URL proof.

**Path parameters**

<ParamField path="courseId" type="string" required>
  Canvas course ID.
</ParamField>

<ParamField path="quizId" type="string" required>
  Content ID in canonical form.
</ParamField>

**Request body**

<ParamField body="configKeyHash" type="string">
  SHA-256 hash of the SEB Config Key for the current browser URL, as provided by the SEB JavaScript API (`SafeExamBrowser.security.configKeyHash`).
</ParamField>

<ParamField body="url" type="string">
  Current browser URL reported by SEB. Must be a valid HTTPS URL on the configured Canvas origin, matching the Classic Quiz take page or New Quiz assignment page for this course and content ID.
</ParamField>

**Success response — 200**

```json theme={null}
{
  "success": true,
  "proofToken": "<43-char base64url token>",
  "expiresInSeconds": 120
}
```

<ResponseField name="proofToken" type="string">
  A 43-character base64url one-time token. Pass this token in the `x-seb-proof-token` header of `POST /api/seb/access-code`.
</ResponseField>

<ResponseField name="expiresInSeconds" type="number">
  Proof token lifetime. Always `120`.
</ResponseField>

**Error responses**

| Status | `error_code`               | Meaning                                                                                                                                      |
| ------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `403`  | `INVALID_SEB_CONFIG_PROOF` | Config Key hash does not match, URL is not a recognised quiz page, or the configuration is stale. Student must download a fresh `.seb` file. |
| `404`  | —                          | No SEB setting found for this assessment.                                                                                                    |
| `429`  | `RATE_LIMITED`             | Per-process or per-IP proof request budget exceeded.                                                                                         |

The response carries `cache-control: no-store`, `pragma: no-cache`, and related sensitive-response headers on every response.

***

### POST /api/seb/access-code/:courseId/:quizId

Redeems a proof token and returns the Canvas access code, approved exam tool list, and an exit grant.

The server atomically consumes the proof token, verifies the course, content, and settings fingerprint match, confirms the access code is still set, and generates an exit grant before returning the response.

**Authentication:** None (public endpoint). The proof token is the only credential.

**Path parameters**

<ParamField path="courseId" type="string" required>
  Canvas course ID.
</ParamField>

<ParamField path="quizId" type="string" required>
  Content ID in canonical form.
</ParamField>

**Request headers**

<ParamField header="x-seb-proof-token" type="string" required>
  The `proofToken` value returned by `POST /api/seb/access-proof`. Must be a 43-character base64url string. The server rejects requests without a syntactically valid token before any database access.
</ParamField>

**Success response — 200**

```json theme={null}
{
  "success": true,
  "accessCode": "ab12cd34",
  "exitGrant": "<43-char base64url token>",
  "exitGrantExpiresInSeconds": 43200,
  "tools": [
    {
      "id": "tool-abc123",
      "label": "Reference Sheet",
      "url": "https://example.com/reference.pdf"
    }
  ]
}
```

<ResponseField name="accessCode" type="string">
  The current Canvas access code for this assessment. The detector fills only an unambiguous Canvas access-code prompt and never treats DOM content as authorisation.
</ResponseField>

<ResponseField name="exitGrant" type="string">
  A 43-character base64url token used to validate the post-submission exit page and the quit redirect. Valid for 12 hours (43 200 seconds).
</ResponseField>

<ResponseField name="exitGrantExpiresInSeconds" type="number">
  Exit grant lifetime. Always `43200` (12 hours).
</ResponseField>

<ResponseField name="tools" type="array">
  Approved external tools for this assessment. Each entry has `id`, `label`, and `url`. Empty array when no tools are configured or the SEB setting is not fully active.
</ResponseField>

**Error responses**

| Status | Meaning                                                                                               |
| ------ | ----------------------------------------------------------------------------------------------------- |
| `403`  | Proof token is missing, malformed, expired, already consumed, or course/content/fingerprint mismatch. |
| `404`  | No SEB setting found for this assessment.                                                             |
| `429`  | Per-process or per-IP request budget exceeded.                                                        |

The response carries sensitive no-store response headers.

***

### GET /api/seb/access-code/:courseId/:quizId

Returns a `405` error. Redemption is always `POST`-only; this route exists to provide an explicit method error rather than a generic 404.

***

## Approved Tools Under Session Boundary

### GET /api/seb/tools/:courseId/:quizId

Returns the current approved tool list for an assessment while a verified LTI session is active. The detector uses this to refresh tool availability after an access-code redemption.

**Authentication:** Requires a verified LTI principal for the given `courseId`.

**Path parameters**

<ParamField path="courseId" type="string" required>
  Canvas course ID.
</ParamField>

<ParamField path="quizId" type="string" required>
  Content ID in canonical form.
</ParamField>

**Success response — 200**

```json theme={null}
{
  "success": true,
  "tools": [
    {
      "id": "tool-abc123",
      "label": "Allowed Calculator",
      "url": "https://example.com/calc"
    }
  ]
}
```

Returns `tools: []` when no tools are configured, or when SEB is not required, not enabled, or the effective quit password is absent.

***

## Session Readiness

These two endpoints support an optional prompt that asks students to verify their Canvas session connection before the exam configuration is downloaded.

### POST /api/seb/session-readiness

Checks whether the student's stored Canvas OAuth grant can produce a session URL. A successful response proves the Canvas connection is ready without retaining or exposing the session URL itself.

**Authentication:** Requires a verified student LTI principal.

**Success response — 200**

```json theme={null}
{
  "success": true,
  "checks": { "canvasSessionAuthorization": true },
  "handoffUrl": "/seb/launch-handoff?key=<token>"
}
```

**Error responses**

| Status | `error_code`                            | Meaning                                                      |
| ------ | --------------------------------------- | ------------------------------------------------------------ |
| `403`  | `LTI_STUDENT_REQUIRED`                  | Not a verified student session.                              |
| `403`  | `CANVAS_SESSION_AUTHORIZATION_REQUIRED` | OAuth grant is missing or expired; student must reauthorise. |
| `502`  | `CANVAS_SESSION_READINESS_FAILED`       | Canvas returned an unexpected error.                         |
| `503`  | `CANVAS_SESSION_UNAVAILABLE`            | Canvas API service is not configured.                        |

***

### POST /api/seb/session-readiness/dismiss

Records the student's preference to dismiss the readiness prompt. This preference is stored per Canvas user and is not a device trust record.

**Authentication:** Requires a verified student LTI principal.

**Success response — 200**

```json theme={null}
{ "success": true }
```

***

## Setup Check Flow

The setup check is a separate SEB configuration that verifies certificate decryption, SEB detection, connectivity, and Config Key proof without releasing any assessment access code or establishing device trust.

### GET /seb/check/config.seb

Downloads the setup-check SEB configuration. Unlike assessment configurations, this file is generated once and reused from an in-process cache across requests (the encrypted bytes are stable because the setup-check settings never change). Certificate readiness is re-verified on every response even when the cached bytes are served.

The setup-check configuration starts at `/seb/check`, allows quit without an assessment exit password, and cannot be used to redeem an access-code proof.

**Authentication:** None. Rate-limited per process and per IP.

***

### GET /seb/check

Renders the setup-check page inside SEB. The page runs a Config Key proof against `/api/seb/check-proof` and reports the results to the student.

***

### POST /api/seb/check-proof

Verifies that SEB is running the setup-check configuration.

**Request body**

<ParamField body="configKeyHash" type="string">
  Config Key hash from the SEB JavaScript API for the current URL.
</ParamField>

<ParamField body="url" type="string">
  Current browser URL; must match `/seb/check` on the configured application base URL.
</ParamField>

**Success response — 200**

```json theme={null}
{
  "success": true,
  "checks": { "configKey": true, "expectedUrl": true }
}
```

**Error response**

| Status | `error_code`                    | Meaning                                                                           |
| ------ | ------------------------------- | --------------------------------------------------------------------------------- |
| `403`  | `INVALID_SEB_SETUP_CHECK_PROOF` | Config Key or URL did not match. Student must reopen the setup check from Canvas. |

***

### GET /seb/check/quit

Renders the quit page for the setup check. Sets `x-seb-quit: true` and `x-seb-exit: setup-check` response headers so SEB can close the browser after the student dismisses the page.

***

## SEB-Required Assessment View

### GET /seb/quiz/:courseId/:quizId

Renders the "Safe Exam Browser Required" download page for a Classic Quiz when the student opens a quiz URL in a normal browser. This route is only for Classic Quizzes; it rejects New Quiz content IDs and redirects non-SEB-required assessments directly to Canvas.

**Authentication:** Requires a verified LTI principal for the given `courseId`.

**Path parameters**

<ParamField path="courseId" type="string" required>
  Canvas course ID.
</ParamField>

<ParamField path="quizId" type="string" required>
  Classic Quiz ID (numeric string). New Quiz content IDs return `403`.
</ParamField>

**Query parameters**

<ParamField query="canvas_url" type="string">
  Original Canvas quiz URL, used to construct the return link.
</ParamField>

<ParamField query="user_id" type="string">
  Canvas user ID, passed through to the React shell.
</ParamField>

***

## Assessment Launch Handoff

### GET /seb/launch/:contentId and POST /seb/launch/:contentId

Handles the LTI assessment launch inside SEB. The `POST` variant accepts an `id_token` and `state` in the request body (standard LTI 1.3 launch); the `GET` variant handles a session-based reload where the principal is already in the Express session.

The handler validates the LTI token (RS256 signature, issuer, audience, nonce, timestamps, deployment ID, target link URI, replay claim, and browser transaction cookie), regenerates the session, stores the verified principal, and either redirects to the Canvas assessment URL (if SEB is not required or the client is already SEB) or renders the SEB-required download page with a fresh config-grant action token.

<Note>
  A completed SEB launch is recorded in the session for up to 24 hours. A direct LTI replay — a second load of the same `seb/launch/:contentId` target — redirects to Canvas course home rather than re-presenting the download prompt, preventing a back-navigation loop.
</Note>

***

### GET /seb/launch/:contentId/login

Proxies OIDC initiation parameters to `/lti/login`. Used when Canvas redirects to the SEB-scoped LTI target for a new OIDC login.

***

### GET /seb/launch-handoff

Consumes a short-lived browser launch handoff token and renders the same-tab launcher page that transitions the normal browser while SEB opens.

**Query parameters**

<ParamField query="key" type="string" required>
  The `handoffUrl` key token issued by `POST /api/seb/config-grant` or `POST /api/seb/session-readiness`.
</ParamField>

The handoff record is consumed on access. After consumption, only the validated Canvas course return URL remains accessible (for clean back-navigation on reload), preventing the `sebs://` configuration URL from being served a second time.

***

## Exit Flows

### GET /seb/exit/session/:courseId/:quizId/:grant

Renders the post-submission exit page after a student completes an assessment. The exit grant is validated against the current SEB setting before the page is displayed.

**Path parameters**

<ParamField path="courseId" type="string" required>
  Canvas course ID.
</ParamField>

<ParamField path="quizId" type="string" required>
  Content ID in canonical form.
</ParamField>

<ParamField path="grant" type="string" required>
  Exit grant token returned in the `exitGrant` field of the access-code response.
</ParamField>

The page links to `GET /seb/exit/quit/:courseId/:quizId/:grant` for the final quit redirect. A `403` with an explanatory message is returned when the grant is missing, expired, or the SEB setting is no longer active.

***

### GET /seb/exit/quit/:courseId/:quizId/:grant

Validates the exit grant and performs an HMAC-authenticated redirect to the SEB quit URL embedded in the assessment configuration. This is the authoritative quit path; it reads the current access code from the database and constructs the configuration-bound quit URL before redirecting.

**Path parameters**

<ParamField path="courseId" type="string" required>
  Canvas course ID.
</ParamField>

<ParamField path="quizId" type="string" required>
  Content ID in canonical form.
</ParamField>

<ParamField path="grant" type="string" required>
  Exit grant token.
</ParamField>

**Response**

`303` redirect to the SEB quit URL on success. `403` with an error page when the grant is invalid or expired.

***

### GET /seb/exit/complete/:courseId/:quizId/:token

Renders the quit-completion page after SEB follows the quit URL. The `token` in the path is validated as an HMAC token derived from the current access code; the page sets `x-seb-quit: true` and `x-seb-exit: submitted` response headers to allow SEB to close.

**Path parameters**

<ParamField path="courseId" type="string" required>
  Canvas course ID.
</ParamField>

<ParamField path="quizId" type="string" required>
  Content ID in canonical form.
</ParamField>

<ParamField path="token" type="string" required>
  HMAC quit token derived from the assessment access code.
</ParamField>

***

### GET /seb/exit/:courseId/:quizId

Renders a non-terminal manual exit page. Used for non-submission or mode-driven exits. No grant is required; the page does not trigger an SEB quit.

***

### GET /seb/exit/quit/:courseId/:quizId and GET /seb/exit/manual/:courseId/:quizId

<Warning>
  These routes deliberately return **410 Gone**. Unbound quit paths — those without a valid, settings-bound exit grant — are intentionally unavailable. Students must use the quit link provided on the post-submission exit page or SEB's native Quit command with the proctor-provided exit password. These routes exist to give a clear failure signal rather than silently succeeding or returning 404.
</Warning>
