LTI 1.3 & SCORM Integration β Testing Guide
Prerequisites
- Access to a Canvas, Moodle, or Blackboard sandbox/dev instance
- Admin access to an Accessible PDF Converter tenant
- The migration has been applied:
supabase migration upor deployed via CI
1. Generate Tool Keys
Before any LTI launch can work, the tool needs at least one RSA key pair in the lti_keys table. Run this once against your Supabase instance:
-- Run in Supabase SQL Editor or via psql-- This inserts a placeholder row. The actual key generation happens at runtime-- via the /api/lti/jwks endpoint when keys are present.Or use the API to generate keys programmatically. You can call generateRsaKeyPair() from a one-off script:
import { generateRsaKeyPair } from './workers/api/src/utils/lti-jwt';
const keys = await generateRsaKeyPair();console.log(JSON.stringify(keys, null, 2));// Insert the output into the lti_keys table:// INSERT INTO lti_keys (kid, public_key_jwk, private_key_jwk, active)// VALUES ('<kid>', '<public_key_jwk>', '<private_key_jwk>', true);Verify the JWKS endpoint returns your key:
curl https://api-pdf.theaccessible.org/api/lti/jwks# Should return: {"keys": [{"kty":"RSA","kid":"...","alg":"RS256","use":"sig","n":"...","e":"AQAB"}]}2. Register a Platform (Canvas Example)
A. In Canvas
- Go to Admin > Developer Keys > + Developer Key > LTI Key
- Configure:
- Key Name: Accessible PDF Converter
- Redirect URIs:
https://api-pdf.theaccessible.org/api/lti/callback - Target Link URI:
https://api-pdf.theaccessible.org/api/lti/callback - OpenID Connect Initiation URL:
https://api-pdf.theaccessible.org/api/lti/login - JWK Method: Public JWK URL
- Public JWK URL:
https://api-pdf.theaccessible.org/api/lti/jwks
- Save β note the Client ID (a long number like
10000000000042) - Set the key state to ON
- Go to Settings > Apps > + App > By Client ID and paste the Client ID
B. In the Accessible PDF Converter
- Log in as a tenant admin
- Go to Tenant Admin > LTI Platform Management (
/tenant-admin/lti) - Click Register Platform and fill in:
- Platform Name:
Canvas Sandbox - Issuer URL:
https://canvas.instructure.com(or your Canvas domain) - Client ID: the Client ID from step A.3
- Authorization Endpoint:
https://<your-canvas>/api/lti/authorize_redirect - Token Endpoint:
https://<your-canvas>/login/oauth2/token - JWKS Endpoint:
https://<your-canvas>/api/lti/security/jwks - Deployment ID: (leave blank for Canvas, or use the deployment ID from the External Tool settings)
- Platform Name:
- Click Register β the tool configuration values will be displayed
Canvas Endpoint Cheat Sheet
| Field | Canvas URL |
|---|---|
| Issuer | https://canvas.instructure.com |
| Auth Endpoint | https://<domain>/api/lti/authorize_redirect |
| Token Endpoint | https://<domain>/login/oauth2/token |
| JWKS Endpoint | https://<domain>/api/lti/security/jwks |
Moodle Endpoint Cheat Sheet
| Field | Moodle URL |
|---|---|
| Issuer | https://<domain> |
| Auth Endpoint | https://<domain>/mod/lti/auth.php |
| Token Endpoint | https://<domain>/mod/lti/token.php |
| JWKS Endpoint | https://<domain>/mod/lti/certs.php |
Blackboard Endpoint Cheat Sheet
| Field | Blackboard URL |
|---|---|
| Issuer | https://blackboard.com |
| Auth Endpoint | https://developer.blackboard.com/api/v1/gateway/oidcauth |
| Token Endpoint | https://developer.blackboard.com/api/v1/gateway/oauth2/jwttoken |
| JWKS Endpoint | https://developer.blackboard.com/api/v1/management/applications/<app-id>/jwks.json |
3. Test LTI Launch
- In Canvas, create a course and add the tool as an External Tool assignment or module item
- Click the tool link β you should see:
- A brief redirect through Canvas OAuth
- The converter loads in an iframe (no header/footer)
- You are automatically signed in
- Verify in the database:
lti_user_mappingshas a new row mapping your Canvas user to a Supabase userprofilestable shows the user with the correcttenant_id
Troubleshooting Launch Failures
| Symptom | Likely Cause |
|---|---|
| βUnregistered LTI platformβ | Issuer URL or Client ID doesnβt match registration |
| βJWT verification failedβ | JWKS endpoint unreachable, or key rotation happened (clear KV cache: delete lti-jwks:* keys) |
| βInvalid or expired stateβ | KV state expired (5 min TTL) β retry the launch |
| Blank iframe / X-Frame-Options error | _headers file not deployed β check Cloudflare Pages build |
| βFailed to create sessionβ | Supabase service role key issue β check SUPABASE_SERVICE_ROLE_KEY secret |
4. Test SCORM Export
- Upload and convert a PDF through the normal UI
- Once conversion is complete, call the SCORM export endpoint:
curl -H "Authorization: Bearer <your-token>" \ https://api-pdf.theaccessible.org/api/export/<fileId>/scorm \ -o test-scorm.zip- Verify the ZIP contents:
unzip -l test-scorm.zip# Should contain:# imsmanifest.xml# scorm-api.js# index.html-
Validate the manifest:
- Open
imsmanifest.xmlβ should have valid XML with<schema>ADL SCORM</schema>and<schemaversion>1.2</schemaversion> - The
<resource>element should referenceindex.htmlwithadlcp:scormtype="sco"
- Open
-
Import into an LMS:
- Canvas: Course > Modules > + > External Tool or import as SCORM package via course import
- Moodle: Add Activity > SCORM package > Upload the ZIP
- SCORM Cloud (https://cloud.scorm.com): Free testing tool β upload the ZIP and launch
-
Verify:
- The content displays correctly (styles preserved)
- The SCORM API initializes (
LMSInitializecalled) - On page load,
lesson_statusis set tocompleted - On close,
LMSFinishis called - In the LMS gradebook, the item shows as completed
5. Test Nonce Replay Protection
- Capture a valid LTI launch
id_token(e.g., from browser dev tools Network tab during a launch) - Replay the same token with the same nonce β should get
LTI_NONCE_REPLAYerror - Verify old nonces are cleaned up: run
SELECT cleanup_lti_nonces();in SQL Editor, then checklti_noncestable
6. Test Tenant Admin CRUD
# List platformscurl -H "Authorization: Bearer <token>" \ https://api-pdf.theaccessible.org/api/tenant-admin/lti-platforms
# Createcurl -X POST -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{"name":"Test LMS","issuer":"https://test.example.com","clientId":"123","authEndpoint":"https://test.example.com/auth","tokenEndpoint":"https://test.example.com/token","jwksEndpoint":"https://test.example.com/jwks"}' \ https://api-pdf.theaccessible.org/api/tenant-admin/lti-platforms
# Updatecurl -X PUT -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{"name":"Updated Name","status":"inactive"}' \ https://api-pdf.theaccessible.org/api/tenant-admin/lti-platforms/<platformId>
# Deletecurl -X DELETE -H "Authorization: Bearer <token>" \ https://api-pdf.theaccessible.org/api/tenant-admin/lti-platforms/<platformId>7. Test Embedded UI
- Navigate directly to
/lti/launch?platformId=<id>without LTI tokens β should show βNo LTI session foundβ error - After a successful LTI launch, verify:
- No header/footer/navigation visible
- Upload and conversion work normally
- The page works inside an iframe (test with a simple HTML page that embeds it)
<!-- test-iframe.html β open in browser to test embedding --><iframe src="https://pdf.theaccessible.org/lti/launch?platformId=test" width="100%" height="800" style="border:1px solid #ccc;"></iframe>