CONNECTORS / LTI 1.3

P2LRN LTI 1.3 Module

P2LRN includes a universal, self-hosted LTI 1.3 provider: it validates OIDC/JWT launches, publishes JWKS, offers catalog and Deep Linking, runs the same Player, persists state per learner/attempt, and can deliver grades via AGS. It is not a separate plugin for each LMS nor is it part of the course ZIP.

LTI 1.3 Provider Source Code

The technical ZIP includes the current code of integration/lms: cryptographic provider, HTTP server, SQLite store, AGS client, launch and testing contracts. It also includes the Host Adapter needed to border with Player. It is not a specific Moodle/Canvas/Sakai/Chamilo/Open edX plugin nor a precompiled binary.

Requirements

Node.js 22.13+ and compiled Toolkit and Player distributions. A real deployment also requires a persistent RSA key, private storage, and a trusted HTTPS proxy.

Server

node integration/lms/lti-server.mjs starts the provider after setting its environment variables. By default it listens in loopback.

Security and interoperability

The provider includes JWT validation, issuer/audience, nonce/state, replay protection, Deep Linking, persistence, Player, AGS OAuth and isolation between institutions.

Self-hosted does not mean public shared service

p2lrn.org It is not a multi-institution LTI gateway to register directly to an LMS. Each deployment must host the provider in its own HTTPS URL, persist its keys, and explicitly register its trusted platforms.

Practical guide: connect P2LRN to an LMS

Two coordinated registrations are required: deploy the P2LRN tool provider and register that tool in the LMS. Then register the LMS issuer, identifiers and trusted public keys in P2LRN. Neither downloading a ZIP nor copying a URL automatically makes a course available.

  1. Prepare Node.js, Toolkit, Player, persistent keys and private storage.
  2. Deploy the provider on your own HTTPS domain.
  3. Register an external LTI 1.3 tool in the chosen LMS.
  4. Register the real LMS issuer, client ID, deployment ID and trusted endpoints/JWKS in P2LRN.
  5. Publish the .p2lrn file and attach its content with Deep Linking or an authorized administrative binding.
  6. Test teacher/learner access and, where authorized, official grade submission through AGS.

Where is LTI hosted? The published P2LRN website is documentation, not a ready-to-use multi-tenant LTI tool. You run this provider on a server you control. Its private SQLite store tracks registration, sessions, state and AGS deliveries; this is not the Analytics result-file index and it does not modify a .p2lrn course.

1. Install and configure the LTI 1.3 provider

Source-backed requirements: Node.js 22.13+, the P2LRN repository sources, built Toolkit/Player distributions, a persistent RSA private key, an admin token and access-controlled private storage. The downloadable ZIP contains source code; it is neither an installer nor a configured LTI tool.

From the P2LRN repository root:

Terminal · repository root
cd packages/toolkit
npm ci
npm run build
cd ../..
node packages/player/scripts/build-dist.mjs

Generate and retain a 3072-bit RSA private key (OpenSSL example):

Terminal · private key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out /private/p2lrn/p2lrn-lti.pem

Supply these process environment variables, replacing the sample paths and domain. This is a list of variables, not a .env file automatically loaded by the server.

Environment variables
P2LRN_LTI_BASE_URL=https://lti.example.edu
P2LRN_LTI_DATA_DIR=/private/p2lrn/p2lrn-lti-data
P2LRN_LTI_KEY_FILE=/private/p2lrn/p2lrn-lti.pem
P2LRN_LTI_ADMIN_TOKEN_FILE=/private/p2lrn/admin-token.txt
P2LRN_LTI_PORT=37891
P2LRN_LTI_HOST=127.0.0.1

P2LRN_LTI_ADMIN_TOKEN_FILE must contain a secret random token with at least 32 characters, kept outside the repository. After setting the environment variables, start the server from the repository root:

Terminal · start provider
node integration/lms/lti-server.mjs

The server binds to 127.0.0.1:37891 by default. Expose it through an HTTPS reverse proxy with appropriate access controls. Never publish the private key, SQLite data store, course-storage directory or unprotected /lti/admin/* endpoints.

2. Configure the LTI 1.3 tool inside your LMS

In the LMS administration interface find the equivalent of external LTI 1.3 tools. The menu differs by Moodle, Canvas, Sakai, Chamilo or Open edX release. Choose LTI 1.3, not LTI 1.1, and enter URLs belonging to your P2LRN server:

Tool URLs
Tool / Launch URL: https://lti.example.edu/lti/launch
Initiate Login URL: https://lti.example.edu/lti/login
JWKS / Public keyset: https://lti.example.edu/lti/jwks
Redirect URI for launch: https://lti.example.edu/lti/launch
Deep Linking URL: https://lti.example.edu/lti/deep-link
Deep Linking redirect URI: https://lti.example.edu/lti/deep-link

These are placeholders, not endpoints hosted by p2lrn.org. Register the precise redirect URIs for normal resource launches and Deep Linking. Enable content selection; only request Assignment and Grade Services (AGS) permissions if you need grading.

After registration, obtain the real issuer, client ID, deployment ID, authorization URL, OAuth token URL and public JWKS set from that particular LMS instance. Do not copy example values or assume vendor endpoints are the same in all versions.

See LMS connectors for vendor-specific considerations; an LTI menu item does not in itself certify the complete workflow.

3. Register the trusted LMS platform in P2LRN

This is an administrator operation. Create platform.json with the real values supplied by the LMS. The following only illustrates the accepted JSON shape for POST /lti/admin/platforms; placeholder keys and IDs will not work:

JSON · platform.json
{
  "id": "school-lms",
  "tenantId": "school-001",
  "issuer": "https://campus.example.edu",
  "clientId": "REAL_LMS_CLIENT_ID",
  "authUrl": "https://campus.example.edu/REAL_OIDC_AUTH_URL",
  "tokenUrl": "https://campus.example.edu/REAL_OAUTH_TOKEN_URL",
  "launchUrl": "https://lti.example.edu/lti/launch",
  "deepLinkLaunchUrl": "https://lti.example.edu/lti/deep-link",
  "jwks": { "keys": [
    {"kty":"RSA","kid":"REAL_KEY_ID","n":"REAL_RSA_MODULUS","e":"AQAB"}
  ] },
  "deployments": ["REAL_DEPLOYMENT_ID"],
  "deepLinkReturnOrigins": ["https://campus.example.edu"],
  "status": "ACTIVE"
}

After entering real values, send the file using an administrator Bearer token (POSIX shell example; PowerShell may use Invoke-RestMethod with a protected token and JSON file):

Terminal · admin registration
# Use a real admin token stored in a protected file.
TOKEN="$(cat /private/p2lrn/admin-token.txt)"
curl --fail-with-body -X POST https://lti.example.edu/lti/admin/platforms \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @platform.json

Inspect the response and call GET /lti/admin/platforms with the same Bearer authorization. P2LRN rejects unknown issuers, unapproved deployments and incompatible keys. Never bypass JWT signatures, expiration, nonce or state to work around registration failures.

4. Publish a .p2lrn course for the institution

Export and validate the course in Studio/Toolkit. The administrator sends the actual binary file to POST /lti/admin/catalog; headers select the institution and visible title:

Terminal · publish package
curl --fail-with-body -X POST https://lti.example.edu/lti/admin/catalog \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-p2lrn-tenant: school-001" \
  -H "x-p2lrn-title: Sample course" \
  -H "Content-Type: application/vnd.p2lrn+zip" \
  --data-binary @curso.p2lrn

The server validates package contents, enforces a 64 MiB limit and returns a catalog id, portable packageId, contentRoot and published-entity count. Keep the catalog ID: it differs from the portable package ID. The catalog can be inspected with GET /lti/admin/catalog/school-001 using your Bearer authorization.

When selecting a unit or activity, use an entityId that actually exists in the verified package. Do not invent arbitrary web paths or launch external resources.

6. What happens when a learner opens an activity

  1. The learner signs in to the LMS and opens the teacher's activity.
  2. The LMS calls /lti/login; P2LRN creates a one-use transaction containing state and nonce.
  3. The LMS posts a signed id_token to /lti/launch. The server validates signature, issuer, audience, deployment, expiration and authorized resource binding.
  4. It creates a short-lived session with protected cookies and CSRF checks. The server delivers authorized package content through the same P2LRN Player.
  5. Host Adapter calls /lti/runtime/state/load, /lti/runtime/state/save, /lti/runtime/event and /lti/runtime/score to manage state, events and authorized observations.

State is partitioned by institution, registration, deployment, binding, learner, attempt, package and revision. The LTI implementation uses a private SQLite store for those server records. This does not automatically create or deliver .p2lrn-result files to Analytics; its file-based reporting workflow is separate until explicitly integrated and tested.

7. AGS grades: observation versus official grade

Player may report scores, but a browser score is not automatically a grade in the LMS. After reviewing the attempt, an authorized administrator calls GET /lti/admin/attempts?tenantId=school-001 and approves the grade through POST /lti/admin/grades:

JSON · approve grade
{
  "attemptId": "REAL_ATTEMPT_ID",
  "normalized": 0.85,
  "completed": true
}

The normalized score ranges from 0 to 1. The provider submits via OAuth2 using approved AGS permissions. It may reuse a matching column if the LMS grants lineitem.readonly; creating a new column requires the lineitem scope. Score submission requires the score scope.

Failures remain in GET /lti/admin/ags/pending. An administrator can retry with POST /lti/admin/ags/retry and {"outboxId":"REAL_ID"}. Confirm that the final grade appears in the LMS gradebook; a browser response is not proof of LMS delivery.

8. Test before calling the integration operational

Build Toolkit and Player first, then run the server and contract tests from the repository root:

Terminal · Node tests
node --test integration/lms/lms-contracts.test.mjs
node --test integration/lms/lti-provider.test.mjs
node --test integration/lms/lti-store.test.mjs
node --test integration/lms/lti-ags.test.mjs
node --test integration/lms/lti-server.test.mjs
node --test integration/lms/lti-local-http.test.mjs
node --test integration/lms/package-validation.test.mjs

For the deployed server, check GET /lti/health and GET /lti/jwks, register a real LMS and complete the whole flow with two accounts: teacher selects a course, learner launches and completes an activity, resumes it, and the team verifies state, expiry, access denial and AGS gradebook delivery.

Repeat this on the actual version and installation of Moodle, Canvas, Sakai, Chamilo or Open edX used at your institution. Local contract tests do not certify every LMS version, browser, iframe policy or integration service.

9. Common problems, current limits and security

OIDC/launch rejected (403)

Verify issuer, client ID, deployment, trusted JWT key, audience, expiration and exact registered endpoints. Never trust keys advertised by an unknown token.

Blank iframe or lost session

Check the HTTPS reverse proxy, Secure/SameSite/Partitioned cookies, CSP and browser/LMS third-party cookie or storage policies. HTTP mode is restricted to explicitly configured loopback testing.

No courses in the picker

Confirm the package was published for the correct tenantId, passes validation and is accessible from the approved deployment.

Grade missing from LMS

Verify server-approved attempt, AGS scopes, authorized line item and pending deliveries. A score observation is not an official grade.

Roles and learner rosters

Read-only NRPS 2.0 support is implemented, but real LMS enablement and verification remain necessary.

Source download

The LTI ZIP contains implementation source and tests; you must build dependencies and configure the provider. It is not a standalone installer or a vendor-specific plugin.

Keep secrets out of public web resources, protect private session data, restrict administrator APIs and do not claim full interoperability without real LMS end-to-end tests.

Architecture: single player

LMS→Gateway LTI 1.3→Host Adapter→Player

The LMS maintains learners, teachers, registrations and gradebooks. The gateway verifies the identity and selects the published content; The Host Adapter manages loading and saving of state, events and notes. Player always opens the same .p2lrn, regardless of the LMS manufacturer.

OIDC – Secure Boot and Launch

  1. The administrator explicitly registers the platform using issuer, client_id, allowed deployments, authorization/token URLs and trusted JWKS keys.
  2. /lti/login create a transaction with state and nonce unique and short-lived.
  3. /lti/launch validates the RS256 signature and claims iss, aud/azp, exp, iat, version, type, deployment_id, target_link_uri and authorized binding.
  4. state and nonce They are consumed to block replay before creating the runtime session.

These checks live in the backend. The browser only receives a limited session for already authorized content.

JWKS and public keys

The provider publishes its keyring on /lti/jwks and sign Deep Linking responses with your persistent RSA key. To validate the LMS, it only uses keys belonging to trusted administrative records; a URL received from an unknown JWT does not become a trusted source.

The private keys, administrative token, SQLite, and package catalog remain off the public web and outside of each .p2lrn.

Deep Linking: choose course and lesson

The server implements protected catalog and selector. A teacher can choose a published course, unit, or activity and the provider returns a ltiResourceLink signed with a p2lrn_binding_id opaque tenant-bound, registration, deployment, package, contentRoot and entity.

Identidad portable

packageId + contentRoot + entityId identifies content and revision.

Deployment Binding

The opaque link avoids relying solely on resource_link.id, which can change when copying courses.

No Deep Linking

An administrator can create an authorized manual binding using POST /lti/admin/bindings and configure its opaque value as a custom LMS parameter.

Neither the teacher nor the learner can choose an arbitrary URL or entity to bypass the authorized catalog.

AGS: submit grades

The provider implements Assignment and Grade Services. The browser can report an observation, but an official note is only sent after the server validates it as authoritative. POST /lti/admin/grades records the decision and uses a persistent, idempotent outbox for OAuth2 submission.

Columna existente

With scope lineitem.readonly can locate and reuse a compatible column of the same resourceLinkId, respecting your scoreMaximum.

Create column

A new one is only created when the LMS explicitly granted write scope lineitem.

Retries

The faults remain in /lti/admin/ags/pending and can be retried without accepting redirections or destinations outside the registered LMS.

NRPS 2.0: read course rosters and roles

P2LRN supports optional, read-only Names and Role Provisioning Services 2.0. It can retrieve enrolled learners and instructors, including people who have not yet launched P2LRN. It does not modify enrollments or permanently store learner rosters.

Enable NRPS in the LMS

  1. Register the LTI 1.3 tool and enable Names and Role Provisioning Services if supported by your LMS release.
  2. Grant the scope https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly.
  3. The signed launch must include https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice containing service_versions: ["2.0"] and context_memberships_url.
  4. If the service uses a different HTTPS origin, explicitly authorize it in the platform registration as nrpsAllowedOrigins. P2LRN never accepts a browser-supplied URL.
JSON · signed LTI claim
{
  "https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice": {
    "context_memberships_url": "https://campus.example.edu/courses/123/memberships",
    "service_versions": ["2.0"]
  }
}

Read the roster

An instructor with a verified LTI role can request GET /lti/runtime/nrps/memberships from their session. A learner gets HTTP 403. A provider administrator with a private Bearer token can list discovered contexts or fetch a particular roster:

API · private administration
GET /lti/admin/nrps/contexts?tenantId=school-001
GET /lti/admin/nrps/memberships?tenantId=school-001&registrationId=school-lms&deploymentId=REAL_DEPLOYMENT&contextId=REAL_COURSE
Authorization: Bearer PRIVATE_TOKEN

Use real IDs from the platform registration and signed launch. The response contains the course context, users, roles, enrollment status and any optional profile fields permitted by the LMS. Analytics still reads .p2lrn-result without requiring NRPS; the roster is not proof of authorship of an offline result.

Security and testing status

P2LRN obtains an OAuth2 read-only token, validates context and trusted service origin, processes bounded pagination and verifies membership schemas. No user lists or OAuth secrets are embedded into course files.

Current verification: automated NRPS client and HTTP tests verify the technical contracts, but do not certify real roster retrieval in every LMS. Before enabling NRPS on Moodle, Canvas, Sakai, Chamilo or Open edX, the administrator must verify the registered OAuth2 URL, course context, read-only scopes and permissions, and complete an end-to-end membership request. See the official 1EdTech specification.

Current provider status

Implemented: provider and persistence

createP2LRNLtiProvider(), createP2LRNLtiServer() and persistent SQLite for logs, deployments, catalog, bindings, transactions, sessions, attempts, status, events and AGS outbox.

Implemented: OIDC, JWKS, and Deep Linking

JWT signing/verification, state/nonce/replay, catalog, selector, and signed Deep Linking response.

Implemented: Player, state, and AGS

Authorized runtime, loading/saving state and events, intent inspection, OAuth2 AGS, secure lineitem reuse, and persistent retries.

Implemented: read-only NRPS 2.0

Course roster lookup via OAuth2 with access and context validation. Live LMS E2E testing is still pending.

Actual self-hosted server endpoints

The server implements /lti/login, /lti/launch, /lti/deep-link, /lti/deep-link/confirm, /lti/jwks, /lti/health, runtime and administration. Those routes belong to your provider deployment, not to the static P2LRN website.

Multi-institution security and isolation

  • Cryptographically validate JWT and verify iss, aud, azp, exp, iat, nonce, state, version, message type and deployment_id.
  • Consume each transaction only once; prevent replay and access between institutions or deployments.
  • Partition progress by institution, deployment, link, learner, attempt, package, review, and activity.
  • Do not expose private keys, LMS credentials, personal data, or tokens within a .p2lrn or browser URLs.
  • Refuse any access to unpublished content, non-existent activity or unauthorized service.
  • Verify the results on the server before transmitting official notes.

Implemented teacher and learner flow

Registro administrativo

The administrator registers a platform and explicit deployments, catalog and bindings; unknown senders/deployments are rejected.

Teacher selection

Deep Linking displays only published content and returns a signed resource linked to the review.

learner Launch

OIDC/JWT validates identity and deployment, creates a limited session and opens the same P2LRN Player.

Status and results

The runtime persists state/events per session and attempt. A validated grade decision can be sent to AGS via the outbox.

This loop is covered by the provider's automated HTTP tests. Checking against each actual LMS must also be performed on the installation and version to be used.

Pre-deployment check

  1. Build Toolkit and Player, generate a persistent RSA key and deploy the server over HTTPS; do not expose the administrative token, private key, SQLite, or package directory.
  2. Register issuer, client ID, deployment ID, URLs and JWKS of the real LMS instance; find out /lti/health and /lti/jwks.
  3. Publish a .p2lrn real in the catalog and verify Deep Linking or authorized manual binding.
  4. Test teacher and learner: login, launch, Player, resume, expiration, replay, access denied and course copy.
  5. When there is AGS, check the column and note in the actual book. Do not declare NRPS or universal compatibility where it has not been tested.
  6. Repeat the matrix with questionnaires, 3D games, SCORM/H5P, mobile and cookie/iframe policies of the specific version of the LMS.

More information about the standard at 1EdTech LTI. For platform differences, consult LMS connectors.