Skip to content

LTI 1.3 integrations

ulams speaks LTI 1.3 in both directions, separately for every academy (tenant):

  • Platform side: ulams launches an external tool inside a lesson (topic type LtiLink), receives grades back through Assignment and Grade Services (AGS) 2.0, shares the course member list through Names and Role Provisioning Services (NRPS) 2.0 when you allow it, and lets authors pick content in the tool with Deep Linking 2.0.
  • Tool side: Moodle, Canvas or another LMS launches a ulams course. The learner is signed in, given access to the course, and their course progress is sent back as a grade. Instructors can pick courses with deep linking.

The design and the choice of libraries are recorded in ADR 0012. The package README is at api/packages/lti/README.md.

LTI screen with the Tools and Platforms tabs

/integrations redirects to /integrations/lti. The page has two tabs. Each opens a drawer to add or edit a record, and the drawer shows the ulams URLs to give to the other side, with copy buttons (from GET /api/admin/lti/endpoints).

The table lists each tool’s name, the Client ID and Deployment ID that ulams issued for it, and whether it is enabled.

Fields in the drawer:

Field Required Notes
Name yes
OIDC login URL yes The tool’s login initiation URL
Launch URL yes
Deep linking URL no Enables content picking from the lesson editor
Other redirect URIs no One per line
JWKS URL one of the two The tool’s public key set
Public key (PEM) one of the two If the tool has no JWKS URL
Custom parameters no key=value, one per line, sent with every launch
Share names / Share e-mails no Off by default: the tool receives no name or e-mail unless switched on
Member list (NRPS) no Off by default: lets the tool read the members of the courses it is linked in (see below)
Enabled

Give the tool the values shown in the drawer: issuer, OIDC auth URL, token URL, JWKS URL, and after saving, the client ID and deployment ID.

  1. In Integrations → LTI → Tools, add the tool with its OIDC login URL, launch URL, optional deep linking URL, and its JWKS URL or PEM key. Save.
  2. Copy the issuer, OIDC auth URL, token URL, JWKS URL, client ID and deployment ID from the drawer into the tool’s configuration.
  3. In a course program, add a topic of type LTI link and choose the tool. If the tool has a deep linking URL, the topic form can open the tool in a pop-up window to pick content (POST /api/admin/lti/tools/{tool}/deep-link); the tool’s response creates LtiLink topics. See the creators guide.

When a learner opens the topic, the learner app calls POST /api/lti/launches/{topic} and gets the tool’s OIDC login URL with a single-use login hint valid for two minutes. Roles sent to the tool: admin becomes Administrator and Instructor, tutor becomes Instructor, everyone else Learner.

A tool with Member list (NRPS) switched on gets the Names and Role Provisioning Services 2.0 claim in launches (context_memberships_url) and may ask for the scope https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly at the token endpoint. With that token, GET /api/lti/platform/nrps/{course} returns the course members as application/vnd.ims.lti-nrps.v2.membershipcontainer+json: the learners with access to the course and its authors, each with user_id (the same sub the tool sees in launches), status: Active and roles. Names and e-mails are included only when the tool’s Share names / Share e-mails are on. The list is paged: limit (default and maximum 100) and page; a Link: <...>; rel="next" header points to the next page; role filters by an LTI role (Learner, Instructor, Administrator, or the full URI). A tool can read only courses that contain one of its links, and switching NRPS off stops tokens already issued. The rlid (resource link) filter of the spec is not supported and is ignored.

Scores the tool sends through AGS are stored append-only. A score with activityProgress Completed or gradingProgress FullyGraded marks the topic as finished in the learner’s course progress.

Registering ulams in Moodle (or another LMS)

Section titled “Registering ulams in Moodle (or another LMS)”
  1. In Moodle, add an external tool (LTI 1.3) using the tool URLs from the Platforms drawer: login URL, launch URL, deep linking URL, JWKS URL.
  2. In Integrations → LTI → Platforms, add the platform with Moodle’s issuer (site URL), client ID, deployment ID and its auth.php, token.php and certs.php URLs.
  3. Point resource links at a course. ulams takes the course from, in this order: the custom parameter course_id=<id> (deep linking sets it), ?course=<id> on the target link URI, or the platform’s default course ID.

On a launch, ulams:

  • matches the user by (platform, sub), never by e-mail. A new user is created with the platform’s e-mail, or a placeholder lti-…@lti.invalid address if the e-mail is missing or already taken;
  • assigns tutor to Instructor, ContentDeveloper and TeachingAssistant roles and student to everyone else; nobody becomes an administrator through LTI;
  • grants access to the course through the course access service;
  • remembers the AGS line item if the platform allows score passback;
  • redirects to the learner site with a one-time code (LTI_TOOL_LANDING_URL, default {front}/lti/launch?code={code}&course={course}), which the front exchanges for a session at POST /api/lti/tool/exchange.

When the platform offers Client Side postMessage Storage (lti_storage_target in the login request), the login page first asks the platform’s storage frame (lti.capabilities, then lti.put_data) to keep the nonce of this login under the key lti1p3_<state>. The launch then reads it back (lti.get_data) in a second step and posts it with the id_token to POST /api/lti/tool/launch/verify; a value that is not the nonce of that login is refused, so a launch cannot be replayed into another browser. Only the platform’s frame and origin (the origin of its issuer) are talked to. The state kept by ulams stays the source of truth: when the platform’s storage does not answer within about two seconds per step, or the platform sends no lti_storage_target, the launch continues exactly as before, with no cookie involved either way. Implemented from the 1EdTech LTI Client Side postMessage Storage specification (frame lookup by name or _parent, the frame of a capabilities answer, org.imsglobal. prefixed or plain subjects).

Only instructors can use deep linking to add course links. When a learner finishes a topic (TopicFinished event), ulams queues a job that sends their course progress to the platform as a score out of 100, marked Completed / FullyGraded once the course is complete.

Every tenant has its own RSA key set (2048-bit by default), with private keys encrypted with the tenant APP_KEY. Keys have three states: next (published, not used yet), active (signs everything) and retired (still published for a grace period).

  • Public key set: GET /api/lti/jwks and GET /.well-known/jwks.json, no authentication.
  • Initial keys: ulams:lti:rotate-keys --init, run as the lti_keys step when a tenant is created (see Tenants).
  • Rotation: ulams:lti:rotate-keys, scheduled monthly on the 1st at 03:00. next becomes active, a new next is generated, and retired keys stay in the JWKS for LTI_RETIRED_KEY_GRACE_DAYS (30) days.
Caller Endpoints
Anyone GET /api/lti/jwks, GET /.well-known/jwks.json, GET /api/lti/frame-origins (origins of the enabled tools, cached 5 minutes; used for the front’s CSP)
Admin (lti_manage) GET /api/admin/lti/endpoints; CRUD on /api/admin/lti/tools and /api/admin/lti/platforms; POST /api/admin/lti/tools/{tool}/deep-link
Learner POST /api/lti/launches/{topic}
External tool GET/POST /api/lti/platform/authorize, POST /api/lti/platform/token, /api/lti/platform/ags/{course}/lineitems[/{id}[/scores, /results]], GET /api/lti/platform/nrps/{course}, POST /api/lti/platform/deep-links
External platform GET/POST /api/lti/tool/login, POST /api/lti/tool/launch
Learner’s browser (storage step) POST /api/lti/tool/launch/verify
Learner’s browser / front POST /api/lti/tool/deep-link, POST /api/lti/tool/exchange (throttled to 30 per minute)

See API endpoints.

One permission, lti_manage, controls the whole Integrations menu and every admin endpoint. The LtiPermissionSeeder grants it to the admin role. See Permissions.

All optional (api/packages/lti/src/config.php):

Variable Default Purpose
LTI_ISSUER APP_URL Our issuer and endpoint base
LTI_RETIRED_KEY_GRACE_DAYS 30 How long retired keys stay in the JWKS
LTI_KEY_BITS 2048 RSA key size
LTI_LOGIN_HINT_TTL 120 Seconds
LTI_ID_TOKEN_TTL 300 Seconds
LTI_ACCESS_TOKEN_TTL 3600 AGS access tokens, seconds
LTI_STATE_TTL 600 OIDC state, seconds
LTI_CODE_TTL 60 One-time launch code, seconds
LTI_ALLOW_INSECURE_URLS false Development only: allow http and private addresses for JWKS, OIDC and AGS calls
LTI_HTTP_TIMEOUT 10 Seconds
LTI_JWKS_CACHE_TTL 600 Seconds
LTI_TOOL_LANDING_URL {front}/lti/launch?code={code}&course={course} Where a launched learner lands

See Environment variables.

  • Every inbound JWT is checked for signature, iss, aud, exp, nonce or jti, and deployment ID. Login hints, JWT ids, OIDC state and nonce, and launch codes are single use.
  • Outgoing requests go only to public https addresses, without following redirects, unless LTI_ALLOW_INSECURE_URLS is on.
  • Hints, AGS tokens, deep-linking data and keys of one tenant are rejected by another.
  • Launches are audited in the lti_launches table.
  • The launch audit log and AGS scores are stored but not shown in the admin panel.
  • Tool-side grade passback sends course progress, not individual quiz scores.