Letting an instructor launch CEO Sim from any LMS without manual user sync.
constraint: The OIDC handshake requires nonce + state stored across two HTTP hops. The id_token JWT must be validated against the LMS JWKS endpoint (different per platform). LMS role claims differ: Canvas's Instructor is not Moodle's Instructor in the same JSON shape. Deep linking lets instructors target a specific round.
LtiSsoService handles the OIDC initiation (nonce stored in Redis), the launch endpoint validates the id_token JWT, and LtiUserMappingRepository provisions or reuses a User idempotently. resolveRoleId() maps LMS role URIs to internal role_ids. LtiDeploymentRepository persists context + NRPS/AGS URLs per deployment. A ValidateLtiLaunch middleware intercepts every launch.
// On launch (id_token already validated):
$platform = LtiPlatformRepository::findByIssuerAndClientId($issuer, $clientId);
$mapping = LtiUserMappingRepository::findByPlatformAndLtiUserId(
$platform->id, $claims['sub']
);
if (! $mapping) {
$user = User::firstOrCreate(['email' => $claims['email']], [
'name' => $claims['name'],
'password' => Hash::make(Str::random(16)),
]);
$mapping = LtiUserMappingRepository::create([
'lti_platform_id' => $platform->id,
'lti_user_id' => $claims['sub'],
'user_id' => $user->id,
'lti_roles' => $claims['roles'],
]);
}
$user->update(['role_id' => $this->resolveRoleId($claims['roles'])]);
return new JwtResponse($user, $this->getToolToken($user));