Safe Online Exam
Troubleshooting

OAuth & scopes

Authorization problems — scopes that do not appear in the Canvas UI, repeated authorization prompts, and students who cannot connect Canvas.

These appear during the Canvas OAuth "Connect Canvas" flow, or when the tool tries to use the Canvas API on someone's behalf.

A scope does not appear in the Developer Keys UI

Some Canvas environments do not surface every endpoint scope in the UI. This most often affects url:GET|/api/v1/login/session_token, which the SEB session handoff requires.

Use the API OAuth key, not the LTI key

Update the API OAuth Developer Key configured as CANVAS_API_CLIENT_ID — not the separate LTI 1.3 Developer Key. The exact scope is url:GET|/api/v1/login/session_token. Do not substitute url:GET|/login/session_token, a logins scope, or a generic login permission.

Canvas's normal editor can omit this scope. Use the root-account Developer Keys REST API to read the existing key, merge the required scope into its current list, and write that list back. Canvas documents PUT /api/v1/developer_keys/:id and developer_key[scopes] for this purpose.

Create a temporary administrator token

Have a root-account Canvas administrator create a short-lived personal access token with permission to manage Developer Keys. In Terminal, start bash, then run the following preflight. Replace the Canvas origin and API OAuth Developer Key ID; 1 is the usual root account ID but must match your Canvas root account.

set -euo pipefail

export CANVAS_ORIGIN='https://your-school.instructure.com'
export ROOT_ACCOUNT_ID='1'
export DEVELOPER_KEY_ID='REPLACE_WITH_THE_API_OAUTH_KEY_ID'
export SESSION_SCOPE='url:GET|/api/v1/login/session_token'

read -r -s -p 'Temporary root-admin Canvas token: ' CANVAS_ADMIN_TOKEN
printf '\n'

KEY_JSON="$(
  curl --fail-with-body --silent --show-error \
    -H "Authorization: Bearer ${CANVAS_ADMIN_TOKEN}" \
    "${CANVAS_ORIGIN}/api/v1/accounts/${ROOT_ACCOUNT_ID}/developer_keys?per_page=100" |
  jq -ce --arg id "${DEVELOPER_KEY_ID}" '
    map(select((.id | tostring) == $id)) |
    if length == 1 then .[0] else error("Developer Key not found") end
  '
)"

printf '%s\n' "${KEY_JSON}" | jq '{id, name, is_lti_key, require_scopes, scopes}'

Confirm the key before changing it

Confirm that the displayed key is the intended API OAuth key and that "is_lti_key": false. If it is an LTI key, stop and locate the API OAuth key instead.

Merge the scope, update the key, and verify

Run this in the same bash session. It changes only the key's scopes property and preserves every scope already present.

EXISTING_SCOPES="$(printf '%s' "${KEY_JSON}" | jq -c '.scopes // []')"

UPDATED_SCOPES="$(
  printf '%s' "${EXISTING_SCOPES}" |
  jq -c --arg scope "${SESSION_SCOPE}" '
    if index($scope) then . else . + [$scope] end
  '
)"

PAYLOAD="$(jq -cn --argjson scopes "${UPDATED_SCOPES}" '{developer_key: {scopes: $scopes}}')"

UPDATED_KEY="$(
  curl --fail-with-body --silent --show-error \
    -X PUT \
    -H "Authorization: Bearer ${CANVAS_ADMIN_TOKEN}" \
    -H 'Content-Type: application/json' \
    --data "${PAYLOAD}" \
    "${CANVAS_ORIGIN}/api/v1/developer_keys/${DEVELOPER_KEY_ID}"
)"

printf '%s\n' "${UPDATED_KEY}" |
  jq '{id, name, require_scopes, scopes}'

printf '%s\n' "${UPDATED_KEY}" |
  jq -e --arg scope "${SESSION_SCOPE}" '.scopes | index($scope) != null' >/dev/null

echo 'Verified: session-token scope is present.'
unset CANVAS_ADMIN_TOKEN KEY_JSON EXISTING_SCOPES UPDATED_SCOPES PAYLOAD UPDATED_KEY

Revoke the temporary token and reconnect users

Immediately revoke the temporary administrator token in Canvas. Do not reopen and save this key through the normal scope UI afterward; that UI may omit the hidden scope again.

Every user who connected before this change — administrators, instructors, and students — must select Reconnect Canvas and authorize once. Existing OAuth tokens do not gain new Developer Key scopes automatically. See Canvas Developer Key scopes.

The application already requests this exact scope in its API OAuth configuration. Canvas uses it for the official session-start endpoint, which returns a short-lived browser session URL for quiz taking. See Canvas OAuth session-token documentation and the full API OAuth key scope list.

Course reset repeatedly asks to reconnect

Confirm that the same API OAuth Developer Key also permits the administrator-only scope url:GET|/api/v1/courses/:course_id/quizzes/:id. A root administrator must select Reconnect Canvas once after that scope is added; instructor and student connections do not need to change for this administrator capability. The reset uses this exact read scope to record each Classic Quiz access code before it makes any change.

The authorization window does not return to the tool

Authorization must start from the Safe Online Exam page opened by a signed LTI launch. The service opens Canvas authorization in a popup and, after the callback, resumes the original role-appropriate page through an exact-origin popup handshake; the callback itself does not render a privileged workspace.

Fix: allow the popup for the tool and Canvas origins, begin again from the course or account-navigation tool entry, and do not bookmark or open the callback URL directly. If the browser cannot complete the popup exchange, reopen the tool from Canvas and retry the connection.

Instructor is asked to authorize repeatedly

The OAuth callback or key configuration does not match.

Fix — confirm all three:

  • The API OAuth redirect URI exactly matches ${TOOL_URL}/api/oauth2callback.
  • The OAuth key is enabled.
  • The configured CANVAS_API_CLIENT_ID / CANVAS_API_CLIENT_SECRET are the API-key values — not the LTI key's.

If you changed the redirect URI or scope set, affected users must reauthorize once.

Student cannot connect Canvas or the configuration download fails

Usually a missing scope or a Canvas-environment mismatch.

Fix:

  • Confirm the exact session_token scope is allowed on the API key.
  • Confirm the student is authorizing the same Canvas environment as the LTI launch — connecting a different Canvas will not satisfy the launch.
  • Have the student run the setup check to test the missing/revoked-scope recovery path, which requests a fresh Canvas connection.

Why every connection requests the same scopes

By design, every Canvas connection requests the same complete application scope set regardless of the user's role in the initiating course. This keeps one durable grant valid for someone who is an instructor in one course and a student in another. Canvas still enforces their actual permissions, so a broad scope request does not grant broad access — it only avoids incompatible role-specific grants. An administrator's grant is the same record, upgraded in place with the account scopes.

Where tokens live

One OAuth grant is stored per Canvas user in canvas_oauth_tokens; the service refreshes it as needed, and a 401 from Canvas triggers at most one safe refresh/retry. If authorization is badly wedged for one user, having them Reconnect Canvas issues a fresh grant. Rotating STATE_ENCRYPTION_KEY invalidates in-flight OAuth state (not stored grants) — see Configuration → Secret rotation.

On this page