Important status
This page specifies proposed work. The backend has not been changed.
This proposal is tied to the pinned backend commit above. It distinguishes confirmed code behavior from product decisions that must be settled before implementation. This is the engineering companion to the feasibility assessment (Report 02): that report holds the product context, scope, sizing, and decisions; this document holds the proposed file-by-file changes. With no dedicated LTI SME today, treat it as a well-researched starting point for the implementing team — a reference to build on with your own review and judgment, not a mandate.
01 / TARGET OUTCOME
Let instructors access SolidProfessor through LTI
The scope is instructor access through the school's existing LTI integration. When an instructor selects the SolidProfessor tool in the LMS, the validated launch should sign them into SolidProfessor and create, connect, or promote their account just in time. Returning launches should recognize and reuse that same instructor account.
Expected behavior
Just-in-time access when it is needed
- Launch directly from the LMS into SolidProfessor
- Create or connect the instructor on first use
- Recognize the same instructor on later launches
- Use the signed standard
roles claim
- Keep
sub as the user identifier
- Keep
lms_school_id for school mapping
Position in the onboarding model
One supported option—not the only option
- Supports on-demand instructor onboarding through LTI
- Does not replace SSO, rostering, or manual provisioning
- Does not create districts or schools
- Does not import every teacher in advance
- Does not implement NRPS or OneRoster
- Does not replace existing licensing decisions
Exact signed values: read https://purl.imsglobal.org/spec/lti/claim/roles and elevate only when its array contains http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor. The current backend's captured Schoology launch already contains that standard value.
02 / SCOPE BOUNDARY
Instructor access—not institution provisioning
LTI just-in-time access is one way to support instructors. It can be made available across an existing mapped school, but each instructor is created or connected only when they launch the tool. A district may still use SSO, roster sync, or administrative provisioning alongside this option.
01
District / school setup
Create the SolidProfessor account and schools, configure licensing, register the LTI deployment, and map LMS school IDs.
Pre-existing
02
LTI instructor launch
Validate the signed launch, resolve the mapped school and role, then create or connect the instructor just in time.
This proposal
03
Returning instructor launch
Recognize the existing school user by the stable LTI identifier and allow the instructor to continue without creating a duplicate account.
This proposal
04
Course enrollment
Enroll the resulting school user when a SolidProfessor class is present in the target URL.
Existing
05
District roster sync
Pre-create schools, courses, users, and enrollments before launches through OneRoster, SIS, or another feed.
Out of scope
03 / TARGET FLOW
Five guarded steps from launch to instructor
The Packback library already validates the LTI launch before application provisioning begins. The new logic should consume only that validated launch data.
01Validate
Verify signature, issuer, client, deployment, nonce, and message through the existing LTI library.
02Scope school
Resolve lms_school_id only within the deployment that validated the launch.
03Resolve role
Exact Instructor context role maps to SolidProfessor Instructor. Everything else remains Student.
04Provision or reuse
Create or connect the instructor on first use; recognize and reuse the same school user on later launches.
05Authorize
Issue the normal LTI access token and avoid sending a valid instructor through a student payment path.
04 / FILE CHANGE INVENTORY
Backend touch points
These are the expected production and test changes. A final file count depends on the licensing decision described later.
01
Launch/Enums/LtiContextRole.php
New allowlist for the standard roles claim and exact Instructor, Learner, and Teaching Assistant context-role URIs.
New
02
Launch/Actions/ResolveLtiProvisioningRole.php
New action that maps only the exact Instructor URI to Role::INSTRUCTOR; missing, malformed, or unrecognized roles fall back to Student.
New
03
Launch/Actions/FindSchoolByLmsId.php
Accept the validated deployment and query only its mapped schools. Do not resolve an LMS school ID through another deployment.
Security
04
Launch/Actions/NormalizeLtiUserIdentifier.php
New compatibility helper used by resource launch, Deep Linking, and account connection so Schoology-style sub values are handled consistently.
New
05
Launch/Actions/FindOrCreateUserForLtiContext.php
Resolve the launch role once; provision new instructors; promote an existing Student on a valid Instructor launch; never demote existing Instructor or administrator roles.
Core
06
Launch/Actions/DoLtiDeepLinkLaunch.php
Require a signed Instructor role for automatic provisioning and reuse the central provisioning action when the instructor's first interaction is Deep Linking.
Core
07
Api/v1/.../LtiConnectController.php
Use the same identifier normalization helper as both launch paths when linking an existing account.
Modify
08
Launch/Actions/DoLtiResourceLinkLaunch.php
Make the inactive-user/payment redirect role-aware so a newly recognized instructor is not treated as an inactive known-LMS student.
Policy
09
Mapping request + database migration
Reject duplicate lms_school_id values within one deployment and add a unique deployment/mapping constraint after auditing existing data.
Hardening
10
LtiTestHelpers.php + LTI system tests
Add signed-role fixtures and regression coverage for role mapping, mapping scope, Deep Linking, identity normalization, and payment behavior.
Tests
11
Academic/Lti/Setup/Contracts/Lti13Service.php
Remove three dd($e) calls at lines 154, 176, 191. Replace with proper error handling and logging. These debug statements halt execution and expose sensitive data.
Gap found
12
UpdateLtiRegistrationDeploymentMappingRequest.php
Add uniqueness validation for lms_school_id within a deployment. Current rules only check ['nullable', 'string']—no duplicate prevention.
Gap found
No change expected in ProvisionUserToSchool.php: it already accepts Role::INSTRUCTOR, synchronizes that role, and can create the instructor's default class. The caller must supply the safe role and activation policy. (Update: ASE-1221 has since modified this file for an unrelated primary-account fix — see the addendum below.)
04b / UPDATE SINCE THIS PROPOSAL
Re-reviewed at cc6a47a28 — what landed since, and what to fold in
This proposal is pinned to baseline c8799473d5a5 and remains accurate against it. Re-reviewed against main at cc6a47a28 (Sept 2026): the conclusions still hold — the LTI roles claim is still unread, and new users still default to Student. Two things to fold in: changes that landed in ASE-1221, and additional touch points surfaced by the feasibility review (Report 02).
Changes that landed since (ASE-1221)
- Item #05's file was refactored.
FindOrCreateUserForLtiContext.php now has a flag-gated (ACADEMIC_LTI_PRESERVE_MIGRATED_MEMBER_STATUS) path that claims an unlinked migrated member and preserves their status and role, and the new-user role is now a ternary. The #05 change (resolve role once, provision/promote, never demote) must integrate with this structure — there are now two privileged-role decision points plus the flag. No conflict: the roles claim is still unread.
ProvisionUserToSchool.php did change. ASE-1221 added a primary-account fix (match null or false) that was 500ing LTI launches. The "no change expected" note still holds in intent (it accepts INSTRUCTOR and creates the default class), but the file has since changed for that unrelated reason.
- Item #08's scope narrows. The migrated known-LMS-inactive symptom that partly motivated the role-aware redirect is now separately fixed by ASE-1221's status preservation. #08 is now specifically: do not route a valid instructor to the student payment path.
- Item #11 line numbers. The
dd($e) calls are at 154 / 176 / 191 at current HEAD (Lti13Service.php was untouched by ASE-1221; the pinned-baseline "175, 190" were correct for c8799473d5a5). The inventory row above has been updated to current HEAD.
Additional touch points from the feasibility review (Report 02)
13
UpdateOrCreateLtiLaunch.php
Stop persisting the entire launch payload ('request' => $launchData) to the DB; whitelist non-PII keys. This is the primary "no PII in our database" surface for privacy-strict districts (e.g. LAUSD).
Privacy
14
Setup/Contracts/Lti13Service.php
Guard the accepts_pii auto-flip (set from the presence of an email claim) so a stray claim can't silently un-mask an anonymous deployment.
Hardening
15
DeepLinking/Actions/CreateDeepLinks.php
Attach lms_school_id (and any needed custom params) to deep-linked resources. Schoology does not re-inject app/district custom params, and the later resource launch requires lms_school_id or it throws.
Required
16
Launch/Actions/FindOrCreateUserForLtiContext.php
Give an anonymous instructor a distinct display name ("Anonymous Instructor") so CS can find and deactivate a specific one in admin.
UX
17
Launch/Actions/FindOrCreateUserForLtiContext.php
Activation rule: provision net-new instructors active (an inactive academic instructor resolves to zero permissions); never reactivate an existing email-matched account on launch — deactivation is CS's access-control lever.
Policy
Full context: these additions are detailed in the
feasibility assessment (Report 02), which covers the known and anonymous (LAUSD / Schoology) cases, production evidence, and sizing.
05 / PROPOSED CODE SHAPE
Small, isolated decisions—not role logic scattered across controllers
These snippets illustrate the intended contracts. They are proposed implementation shapes, not code already present in the backend.
1. Resolve the standard context role
new action
final class ResolveLtiProvisioningRole
{
public function handle(array $launchData): string
{
$roles = Arr::wrap(Arr::get(
$launchData,
LtiContextRole::CLAIM,
[]
));
return in_array(LtiContextRole::INSTRUCTOR, $roles, true)
? Role::INSTRUCTOR
: Role::STUDENT;
}
}
2. Preserve privileged users while allowing promotion
FindOrCreateUserForLtiContext
$launchRole = ResolveLtiProvisioningRole::run($launchData);
$roleToApply = $schoolUser
->accountUser
->roles()
->whereIn('name', [
Role::SUPER_ADMIN,
Role::DISTRICT_ADMIN,
Role::INSTRUCTOR,
])
->exists()
? null
: $launchRole;
3. Scope school lookup to the validated deployment
FindSchoolByLmsId
public function handle(
LtiRegistrationDeployment $deployment,
array $custom
): School {
$lmsSchoolId = Arr::get($custom, 'lms_school_id');
return $deployment
->schools()
->wherePivot('lms_school_id', $lmsSchoolId)
->firstOr(fn () => throw new SchoolNotLinkedToDeployment);
}
4. Use one identifier rule everywhere
shared helper
final class NormalizeLtiUserIdentifier
{
public function handle(string $subject): string
{
return Str::of($subject)->before('::')->toString();
}
}
5. Provision first-time Deep Linking instructors
DoLtiDeepLinkLaunch
$launchData = $ltiMessageLaunch->getLaunchData();
$role = ResolveLtiProvisioningRole::run($launchData);
throw_unless(
$role === Role::INSTRUCTOR,
new InstructorRoleRequiredForDeepLinking
);
$schoolUser = FindOrCreateUserForLtiContext::run(
$deployment,
$launchData
);
Compatibility choice: centralizing the existing before('::') behavior fixes today's resource/Deep Linking mismatch without orphaning existing Schoology users. A later, spec-oriented identity migration should store the exact sub with issuer and deployment scope; that is intentionally outside this phase.
06 / SECURITY CONTRACT
Rules that make role mapping safe
An LMS role becomes a SolidProfessor permission boundary. These constraints should be treated as acceptance criteria, not optional polish.
Trust boundaryValidated claims only
Read roles only from launch data returned after Packback validation. Never accept a browser field or custom is_instructor parameter.
AllowlistOnly exact Instructor elevates
Learner, Administrator, Teaching Assistant, unknown URIs, empty arrays, and malformed values must not grant Instructor or administrator access.
Role lifecyclePromote, never auto-demote
A signed Instructor launch can promote a Student. A later Learner launch must not silently remove Instructor, District Admin, or Super Admin privileges.
Tenant boundaryDeployment-scoped school mapping
The LMS school ID must resolve only inside the deployment that validated the launch, and one deployment cannot map the same LMS school ID twice.
PrivacyPII remains optional
Anonymous instructors can use a generated internal email and no password. A stable signed sub is still required for a persistent account.
ObservabilityAudit without leaking tokens
Record deployment, mapped school, resolved role, and provisioning outcome. Do not log the raw ID token, full request payload, or PII.
Do not map LMS Administrator to SolidProfessor District Admin or Super Admin. Those roles have different organizational scope and should continue to require an explicit SolidProfessor administrative process.
07 / PRODUCT DECISIONS
Four decisions required before implementation
The code answers how roles work today, but it cannot decide licensing and permission policy. These decisions are owned and tracked in Report 02 §4 (several are already settled there); they are summarized here for the implementer.
DECISION 01Known-LMS instructor activation
New identified LTI users are currently inactive and routed toward student payment. Recommended: define an instructor entitlement rule and never send a valid instructor through the student payment flow.
DECISION 02Context role versus account role
LTI Instructor is course-contextual, while SolidProfessor's Instructor role is synchronized on AccountUser. Confirm that promotion is intentionally durable across the user's mapped school memberships in that account.
DECISION 03Teaching Assistant treatment
Recommended: leave Teaching Assistants out of the scope of this work unless product discovery deems otherwise. Do not elevate TAs by default; add a separate mapping only after SolidProfessor defines the intended permission set.
DECISION 04Non-instructor Deep Linking
Recommended: reject the automatic path for Learner or unknown roles, while preserving the current account-link fallback only where product explicitly wants it.
08 / TEST PLAN
Regression coverage required before rollout
Tests should be written around the role resolver first, then the provisioning and launch flows. Existing Student behavior must remain unchanged.
Exact Instructor URI resolves to Role::INSTRUCTOR.
Learner, Teaching Assistant, Administrator, unknown, empty, and malformed roles resolve to Student.
Multiple roles containing Instructor resolve to Instructor regardless of order.
A new anonymous Instructor receives an anonymous account, Instructor role, active school membership, and LTI ID.
A new known-LMS Instructor follows the approved activation and entitlement policy.
An existing Student is promoted after a valid Instructor launch.
Existing Instructor, District Admin, and Super Admin roles are not downgraded by Learner or missing-role launches.
A school mapping from another deployment cannot satisfy the current launch.
Duplicate LMS school IDs within one deployment fail validation and the database constraint.
Schoology sub values containing :: match consistently in resource, Deep Linking, and connect flows.
An unknown Deep Linking Instructor is provisioned without the manual account-link step.
Learner Deep Linking cannot create or promote an Instructor.
A valid Instructor resource launch does not enter the student payment redirect.
Existing Student resource launch, activation, payment, and class-enrollment tests remain green.
Likely test locations: FindOrCreateUserForLtiContext/*, DoLtiDeepLinkLaunchTest.php, a real DoLtiResourceLinkLaunchTest.php, mapping request tests, and a new focused resolver test.
09 / CODE EVIDENCE
Current backend sources behind this proposal
Links are pinned to the reviewed commit so the proposal remains auditable if the main branch changes.
10 / DEFINITION OF DONE
The backend change is complete when:
- An instructor can enter SolidProfessor directly from the LMS through the existing LTI integration without a separate SolidProfessor sign-in step.
- A first validated Instructor launch can create, connect, or promote the Instructor in the correct mapped school, and later launches reuse that account.
- Learner, Teaching Assistant, Administrator, missing, and malformed roles cannot elevate access.
- Existing privileged roles are never automatically downgraded.
- Resource and Deep Linking paths use one identifier rule.
- School lookup cannot cross deployment boundaries or select an ambiguous mapping.
- Instructor licensing and activation behavior is explicitly approved and tested.
- Current Student LTI behavior and LMS setup parameters remain unchanged.
- No debug functions (
dd(), dump(), ray()) remain in LTI-related files.
- The change is released behind a controlled backend feature flag, tested with both Canvas and Schoology launch payloads, and monitored before broad enablement.