SolidProfessor Research Library ← Research library
Backend plan · Report 02

Safe LTI instructor provisioning

The backend changes required to let instructors access SolidProfessor through an existing LTI integration, with just-in-time account creation or connection on first use and recognition on return.

Important status

This page specifies proposed work. The backend has not been changed.

The plan is tied to the pinned backend commit above. It distinguishes confirmed code behavior from product decisions that must be settled before implementation.

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.

LayerResponsibilityResult
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 plan
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 plan
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.

01

Validate

Verify signature, issuer, client, deployment, nonce, and message through the existing LTI library.

02

Scope school

Resolve lms_school_id only within the deployment that validated the launch.

03

Resolve role

Exact Instructor context role maps to SolidProfessor Instructor. Everything else remains Student.

04

Provision or reuse

Create or connect the instructor on first use; recognize and reuse the same school user on later launches.

05

Authorize

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.

#File or componentRequired changeType
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, 175, 190. 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.
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       // Never demote a protected role.
        : $launchRole; // Student can be promoted to Instructor.

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
    {
        // Phase 1 preserves current Schoology compatibility.
        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 boundary

Validated claims only

Read roles only from launch data returned after Packback validation. Never accept a browser field or custom is_instructor parameter.

Allowlist

Only exact Instructor elevates

Learner, Administrator, Teaching Assistant, unknown URIs, empty arrays, and malformed values must not grant Instructor or administrator access.

Role lifecycle

Promote, 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 boundary

Deployment-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.

Privacy

PII remains optional

Anonymous instructors can use a generated internal email and no password. A stable signed sub is still required for a persistent account.

Observability

Audit 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 choices should be confirmed in the ticket.

DECISION 01

Known-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 02

Context 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 03

Teaching Assistant treatment

Recommended phase-one default: do not elevate Teaching Assistant. Add a separate mapping only after SolidProfessor defines the intended permission set.

DECISION 04

Non-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 plan

Links are pinned to the reviewed commit so the plan remains auditable if the main branch changes.

10 / DEFINITION OF DONE

The backend change is complete when:

  1. An instructor can enter SolidProfessor directly from the LMS through the existing LTI integration without a separate SolidProfessor sign-in step.
  2. A first validated Instructor launch can create, connect, or promote the Instructor in the correct mapped school, and later launches reuse that account.
  3. Learner, Teaching Assistant, Administrator, missing, and malformed roles cannot elevate access.
  4. Existing privileged roles are never automatically downgraded.
  5. Resource and Deep Linking paths use one identifier rule.
  6. School lookup cannot cross deployment boundaries or select an ambiguous mapping.
  7. Instructor licensing and activation behavior is explicitly approved and tested.
  8. Current Student LTI behavior and LMS setup parameters remain unchanged.
  9. No debug functions (dd(), dump(), ray()) remain in LTI-related files.
  10. The change is released behind a controlled backend feature flag, tested with both Canvas and Schoology launch payloads, and monitored before broad enablement.