-
Notifications
You must be signed in to change notification settings - Fork 24
CodeIgniter Sample Implementation
This guide provides detailed implementation instructions for using the Packback LTI 1.3 PHP library with CodeIgniter 4 applications.
WARNING: This implementation was generated using an LLM based on the Laravel implementation. The author has not worked with CodeIgniter in a decade and makes no guarantee of the accuracy of this document. Please direct specific feedback to the issues page for this repo.
- Install the library via Composer:
composer require packbackbooks/lti-1p3-tool
- Configure JWT leeway in a bootstrap file or Base Controller:
<?php
// app/Config/Boot/production.php or in a base controller's constructor
\Firebase\JWT\JWT::$leeway = 5;
In CodeIgniter 4, we'll use:
- Services for implementing the LTI interfaces
- Models for database interactions
- A dedicated LTI library to centralize functionality
First, create a directory for your LTI services:
mkdir -p app/Services/Lti
Create app/Services/Lti/CiCache.php
:
<?php
namespace App\Services\Lti;
use Packback\Lti1p3\Interfaces\ICache;
use CodeIgniter\Cache\Cache;
class CiCache implements ICache
{
// Cache durations
private const NONCE_EXPIRY = 3600; // 1 hour
private const LAUNCH_DATA_EXPIRY = 86400; // 24 hours
private const ACCESS_TOKEN_EXPIRY = 3600; // 1 hour
private $cache;
public function __construct()
{
$this->cache = \Config\Services::cache();
}
public function getLaunchData(string $key): ?array
{
return $this->cache->get("lti1p3-launch-data-{$key}");
}
public function cacheLaunchData(string $key, array $jwtBody): void
{
$this->cache->save("lti1p3-launch-data-{$key}", $jwtBody, self::LAUNCH_DATA_EXPIRY);
}
public function cacheNonce(string $nonce, string $state): void
{
$this->cache->save("lti1p3-nonce-{$nonce}", $state, self::NONCE_EXPIRY);
}
public function checkNonceIsValid(string $nonce, string $state): bool
{
$cachedState = $this->cache->get("lti1p3-nonce-{$nonce}");
if (!$cachedState) {
return false;
}
// Remove the nonce after validation (one-time use)
$this->cache->delete("lti1p3-nonce-{$nonce}");
return $cachedState === $state;
}
public function cacheAccessToken(string $key, string $accessToken): void
{
$this->cache->save("lti1p3-access-token-{$key}", $accessToken, self::ACCESS_TOKEN_EXPIRY);
}
public function getAccessToken(string $key): ?string
{
return $this->cache->get("lti1p3-access-token-{$key}");
}
public function clearAccessToken(string $key): void
{
$this->cache->delete("lti1p3-access-token-{$key}");
}
}
Create app/Services/Lti/CiCookie.php
:
<?php
namespace App\Services\Lti;
use Packback\Lti1p3\Interfaces\ICookie;
use Config\Services;
class CiCookie implements ICookie
{
private $response;
public function __construct()
{
$this->response = Services::response();
}
public function getCookie(string $name): ?string
{
$request = Services::request();
$cookieValue = $request->getCookie($name);
return $cookieValue ?: null;
}
public function setCookie(string $name, string $value, int $exp = 3600, array $options = []): void
{
$this->response->setCookie(
$name,
$value,
$exp,
'', // path
'', // domain
false, // secure (adjust based on your needs)
true, // httpOnly
'Lax' // sameSite
);
Services::response($this->response);
}
}
Create database models for Issuers and Deployments:
Create app/Models/IssuerModel.php
:
<?php
namespace App\Models;
use CodeIgniter\Model;
class IssuerModel extends Model
{
protected $table = 'issuers';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'issuer',
'client_id',
'key_set_url',
'auth_token_url',
'auth_login_url',
'auth_server',
'tool_private_key',
'kid'
];
public function findByIssuer(string $issuer, ?string $clientId = null)
{
$query = $this->where('issuer', $issuer);
if ($clientId) {
$query->where('client_id', $clientId);
}
return $query->first();
}
}
Create app/Models/DeploymentModel.php
:
<?php
namespace App\Models;
use CodeIgniter\Model;
class DeploymentModel extends Model
{
protected $table = 'deployments';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'issuer_id',
'deployment_id'
];
public function findByDeploymentId(string $issuer, string $deploymentId, ?string $clientId = null)
{
$builder = $this->db->table('deployments d')
->select('d.*')
->join('issuers i', 'i.id = d.issuer_id')
->where('i.issuer', $issuer)
->where('d.deployment_id', $deploymentId);
if ($clientId) {
$builder->where('i.client_id', $clientId);
}
return $builder->get()->getRowArray();
}
}
Create app/Services/Lti/CiDatabase.php
:
<?php
namespace App\Services\Lti;
use App\Models\IssuerModel;
use App\Models\DeploymentModel;
use Packback\Lti1p3\Interfaces\IDatabase;
use Packback\Lti1p3\Interfaces\ILtiDeployment;
use Packback\Lti1p3\Interfaces\ILtiRegistration;
use Packback\Lti1p3\LtiDeployment;
use Packback\Lti1p3\LtiRegistration;
class CiDatabase implements IDatabase
{
private $issuerModel;
private $deploymentModel;
public function __construct()
{
$this->issuerModel = new IssuerModel();
$this->deploymentModel = new DeploymentModel();
}
/**
* Find an LTI registration by issuer and optional client ID
*/
public function findRegistrationByIssuer(string $iss, ?string $clientId = null): ?ILtiRegistration
{
$issuer = $this->issuerModel->findByIssuer($iss, $clientId);
if (!$issuer) {
return null;
}
return LtiRegistration::new([
'issuer' => $issuer['issuer'],
'clientId' => $issuer['client_id'],
'keySetUrl' => $issuer['key_set_url'],
'authTokenUrl' => $issuer['auth_token_url'],
'authLoginUrl' => $issuer['auth_login_url'],
'authServer' => $issuer['auth_server'],
'toolPrivateKey' => $issuer['tool_private_key'],
'kid' => $issuer['kid'],
]);
}
/**
* Find a deployment by issuer, deployment ID, and optional client ID
*/
public function findDeployment(string $iss, string $deploymentId, ?string $clientId = null): ?ILtiDeployment
{
$deployment = $this->deploymentModel->findByDeploymentId($iss, $deploymentId, $clientId);
if (!$deployment) {
return null;
}
return LtiDeployment::new($deployment['deployment_id']);
}
}
If you need LTI 1.1 to 1.3 migration support, extend the Database implementation:
<?php
namespace App\Services\Lti;
use App\Models\Lti1p1KeyModel;
use Packback\Lti1p3\Interfaces\ILtiDeployment;
use Packback\Lti1p3\Interfaces\IMigrationDatabase;
use Packback\Lti1p3\LtiConstants;
use Packback\Lti1p3\Lti1p1Key as Lti1p1KeyClass;
use Packback\Lti1p3\LtiDeployment;
use Packback\Lti1p3\LtiMessageLaunch;
class CiDatabase implements IDatabase, IMigrationDatabase
{
// Include existing IDatabase methods from above, then add:
/**
* Return LTI 1.1 keys that match this launch
*/
public function findLti1p1Keys(LtiMessageLaunch $launch): array
{
$keyModel = new Lti1p1KeyModel();
$keys = $keyModel->findAll();
// Convert to library Lti1p1Key objects
$result = [];
foreach ($keys as $key) {
$result[] = new Lti1p1KeyClass($key['oauth_consumer_key'], $key['secret']);
}
return $result;
}
/**
* Determine if this launch should migrate from LTI 1.1 to 1.3
*/
public function shouldMigrate(LtiMessageLaunch $launch): bool
{
$body = $launch->getLaunchData();
// Check if LTI 1.1 data exists
return isset($body[LtiConstants::LTI1P1]);
}
/**
* Create a 1.3 deployment based on 1.1 data
*/
public function migrateFromLti1p1(LtiMessageLaunch $launch): ?ILtiDeployment
{
$body = $launch->getLaunchData();
$lti1p1Data = $body[LtiConstants::LTI1P1] ?? null;
if (!$lti1p1Data) {
return null;
}
// Find or create issuer
$issuer = $this->issuerModel->findByIssuer($body['iss'], $body['aud'][0] ?? $body['aud']);
if (!$issuer) {
// Create issuer logic here
// $issuerId = $this->issuerModel->insert([...]);
} else {
$issuerId = $issuer['id'];
}
// Create new deployment record
$deploymentId = $body[LtiConstants::DEPLOYMENT_ID];
$this->deploymentModel->insert([
'issuer_id' => $issuerId,
'deployment_id' => $deploymentId,
]);
return LtiDeployment::new($deploymentId);
}
}
Create app/Services/Lti/CiServiceConnector.php
:
<?php
namespace App\Services\Lti;
use GuzzleHttp\Client;
use Packback\Lti1p3\Interfaces\ICache;
use Packback\Lti1p3\LtiServiceConnector;
class CiServiceConnector extends LtiServiceConnector
{
public function __construct(ICache $cache)
{
$client = new Client();
parent::__construct($cache, $client);
// Enable debugging based on environment
if (ENVIRONMENT == 'development') {
$this->setDebuggingMode(true);
}
}
}
Create a central LTI library class to coordinate your LTI operations:
<?php
namespace App\Libraries;
use App\Services\Lti\CiCache;
use App\Services\Lti\CiCookie;
use App\Services\Lti\CiDatabase;
use App\Services\Lti\CiServiceConnector;
use Packback\Lti1p3\DeepLinkResources\Resource;
use Packback\Lti1p3\JwksEndpoint;
use Packback\Lti1p3\LtiException;
use Packback\Lti1p3\LtiGrade;
use Packback\Lti1p3\LtiMessageLaunch;
use Packback\Lti1p3\LtiOidcLogin;
class LtiLibrary
{
private $database;
private $cache;
private $cookie;
private $connector;
public function __construct()
{
$this->database = new CiDatabase();
$this->cache = new CiCache();
$this->cookie = new CiCookie();
$this->connector = new CiServiceConnector($this->cache);
}
/**
* Handle OIDC login request from LMS
*/
public function handleLogin(array $request, string $launchUrl)
{
$login = new LtiOidcLogin($this->database, $this->cache, $this->cookie);
try {
$redirectUrl = $login->getRedirectUrl($launchUrl, $request);
return redirect()->to($redirectUrl);
} catch (LtiException $e) {
log_message('error', 'LTI login error: ' . $e->getMessage());
throw $e;
}
}
/**
* Handle and validate the LTI message launch
*/
public function validateLaunch(array $request)
{
$launch = LtiMessageLaunch::new(
$this->database,
$this->cache,
$this->cookie,
$this->connector
);
try {
return $launch->initialize($request);
} catch (LtiException $e) {
log_message('error', 'LTI launch error: ' . $e->getMessage());
throw $e;
}
}
/**
* Retrieve a previously validated launch
*/
public function getLaunchFromCache(string $launchId)
{
return LtiMessageLaunch::fromCache(
$launchId,
$this->database,
$this->cache,
$this->cookie,
$this->connector
);
}
/**
* Create deep link response
*/
public function createDeepLinkResponse(string $launchId, array $resources = [])
{
$launch = $this->getLaunchFromCache($launchId);
if (!$launch->isDeepLinkLaunch()) {
throw new LtiException('Not a deep linking launch');
}
$dl = $launch->getDeepLink();
// Create resources if none provided
if (empty($resources)) {
$resources = [
Resource::new()
->setUrl(site_url('lti/launch'))
->setTitle('My Resource')
->setText('Resource description')
];
}
// Get JWT for the response
$jwt = $dl->getResponseJwt($resources);
$returnUrl = $dl->returnUrl();
return [
'jwt' => $jwt,
'return_url' => $returnUrl
];
}
/**
* Submit a grade using AGS
*/
public function submitGrade(string $launchId, float $score, string $userId)
{
$launch = $this->getLaunchFromCache($launchId);
if (!$launch->hasAgs()) {
throw new LtiException('Assignments and Grades service not available');
}
$ags = $launch->getAgs();
$grade = LtiGrade::new()
->setScoreGiven($score)
->setScoreMaximum(100)
->setUserId($userId)
->setTimestamp(date('c'))
->setActivityProgress('Completed')
->setGradingProgress('FullyGraded');
return $ags->putGrade($grade);
}
/**
* Get grades for a user (or all users)
*/
public function getGrades(string $launchId, ?string $userId = null)
{
$launch = $this->getLaunchFromCache($launchId);
if (!$launch->hasAgs()) {
throw new LtiException('Assignments and Grades service not available');
}
$ags = $launch->getAgs();
return $ags->getGrades(null, $userId);
}
/**
* Get user roster using NRPS
*/
public function getMembers(string $launchId)
{
$launch = $this->getLaunchFromCache($launchId);
if (!$launch->hasNrps()) {
return []; // Service not available
}
$nrps = $launch->getNrps();
return $nrps->getMembers();
}
/**
* Get course groups
*/
public function getGroups(string $launchId)
{
$launch = $this->getLaunchFromCache($launchId);
if (!$launch->hasGs()) {
return []; // Service not available
}
$gs = $launch->getGs();
return $gs->getGroups();
}
/**
* Get JWKS to verify signatures
*/
public function getPublicJwks()
{
return JwksEndpoint::fromIssuer($this->database, base_url())->getPublicJwks();
}
}
Create a migration file for the required tables:
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateLtiTables extends Migration
{
public function up()
{
// Issuers table
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'issuer' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'client_id' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'key_set_url' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'auth_token_url' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'auth_login_url' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'auth_server' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'tool_private_key' => [
'type' => 'TEXT',
],
'kid' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['issuer', 'client_id']);
$this->forge->createTable('issuers');
// Deployments table
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'issuer_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'deployment_id' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['issuer_id', 'deployment_id']);
$this->forge->addForeignKey('issuer_id', 'issuers', 'id', 'CASCADE', 'CASCADE');
$this->forge->createTable('deployments');
// Optional: LTI 1.1 keys table for migration support
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'oauth_consumer_key' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'secret' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('oauth_consumer_key');
$this->forge->createTable('lti1p1_keys');
}
public function down()
{
$this->forge->dropTable('deployments');
$this->forge->dropTable('issuers');
$this->forge->dropTable('lti1p1_keys');
}
}
Create a base controller for common LTI functionality:
<?php
namespace App\Controllers;
use App\Libraries\LtiLibrary;
use CodeIgniter\Controller;
use Packback\Lti1p3\LtiException;
class LtiBaseController extends Controller
{
protected $ltiLibrary;
public function __construct()
{
$this->ltiLibrary = new LtiLibrary();
}
protected function handleLtiException(LtiException $e)
{
return $this->response->setStatusCode(400)
->setJSON(['error' => $e->getMessage()]);
}
protected function getLaunchFromSession()
{
$session = \Config\Services::session();
$launchId = $session->get('lti_launch_id');
if (!$launchId) {
throw new LtiException('No LTI launch found in session');
}
return $this->ltiLibrary->getLaunchFromCache($launchId);
}
}
<?php
namespace App\Controllers;
use Packback\Lti1p3\LtiException;
class LtiLoginController extends LtiBaseController
{
public function index()
{
try {
return $this->ltiLibrary->handleLogin(
$this->request->getGet(),
site_url('lti/launch')
);
} catch (LtiException $e) {
return $this->handleLtiException($e);
}
}
}
<?php
namespace App\Controllers;
use Packback\Lti1p3\LtiException;
class LtiLaunchController extends LtiBaseController
{
public function index()
{
$session = \Config\Services::session();
try {
$launch = $this->ltiLibrary->validateLaunch($this->request->getPost());
// Store launch ID in session
$session->set('lti_launch_id', $launch->getLaunchId());
if ($launch->isDeepLinkLaunch()) {
return redirect()->to('lti/deep-link');
}
if ($launch->isResourceLaunch()) {
return redirect()->to('lti/resource');
}
if ($launch->isSubmissionReviewLaunch()) {
return redirect()->to('lti/submission-review');
}
return $this->response->setStatusCode(400)
->setJSON(['error' => 'Unknown launch type']);
} catch (LtiException $e) {
return $this->handleLtiException($e);
}
}
}
<?php
namespace App\Controllers;
use Packback\Lti1p3\LtiException;
class LtiDeepLinkController extends LtiBaseController
{
public function index()
{
try {
$launch = $this->getLaunchFromSession();
if (!$launch->isDeepLinkLaunch()) {
throw new LtiException('Not a deep linking launch');
}
$settings = $launch->getDeepLink()->settings();
return view('lti/deep_link', [
'launch' => $launch,
'settings' => $settings
]);
} catch (LtiException $e) {
return $this->handleLtiException($e);
}
}
public function response()
{
try {
$session = \Config\Services::session();
$launchId = $session->get('lti_launch_id');
$response = $this->ltiLibrary->createDeepLinkResponse($launchId);
return view('lti/auto_submit', [
'jwt' => $response['jwt'],
'return_url' => $response['return_url']
]);
} catch (LtiException $e) {
return $this->handleLtiException($e);
}
}
}
<?php
namespace App\Controllers;
class JwksController extends LtiBaseController
{
public function index()
{
return $this->response->setJSON($this->ltiLibrary->getPublicJwks());
}
}
<?php
namespace App\Controllers;
use Packback\Lti1p3\LtiException;
class LtiResourceController extends LtiBaseController
{
public function index()
{
try {
$launch = $this->getLaunchFromSession();
$data = [
'launch' => $launch,
'launchData' => $launch->getLaunchData(),
'members' => [],
'hasNrps' => $launch->hasNrps(),
'hasAgs' => $launch->hasAgs(),
'hasGs' => $launch->hasGs(),
];
if ($launch->hasNrps()) {
$data['members'] = $this->ltiLibrary->getMembers($launch->getLaunchId());
}
return view('lti/resource', $data);
} catch (LtiException $e) {
return $this->handleLtiException($e);
}
}
public function submitGrade()
{
try {
$launchId = \Config\Services::session()->get('lti_launch_id');
$score = $this->request->getPost('score');
$userId = $this->request->getPost('user_id');
$result = $this->ltiLibrary->submitGrade($launchId, $score, $userId);
return $this->response->setJSON(['success' => true, 'result' => $result]);
} catch (LtiException $e) {
return $this->handleLtiException($e);
}
}
public function getGrades()
{
try {
$launchId = \Config\Services::session()->get('lti_launch_id');
$userId = $this->request->getGet('user_id');
$grades = $this->ltiLibrary->getGrades($launchId, $userId);
return $this->response->setJSON(['success' => true, 'grades' => $grades]);
} catch (LtiException $e) {
return $this->handleLtiException($e);
}
}
}
Create app/Views/lti/deep_link.php
:
<!DOCTYPE html>
<html>
<head>
<title>Deep Linking</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.card {
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>LTI Deep Linking</h1>
<div class="card">
<h2>Settings</h2>
<p>Title: <?= $settings['title'] ?? 'N/A' ?></p>
<p>Accept Types: <?= implode(', ', $settings['accept_types'] ?? []) ?></p>
<p>Accept Multiple: <?= $settings['accept_multiple'] ?? false ? 'Yes' : 'No' ?></p>
</div>
<div class="card">
<h2>Select Resource</h2>
<p>Choose a resource to send back to the platform:</p>
<form action="<?= site_url('lti/deep-link/response') ?>" method="post">
<div>
<input type="radio" id="resource1" name="resource" value="resource1" checked>
<label for="resource1">Basic Resource</label>
</div>
<div>
<input type="radio" id="resource2" name="resource" value="resource2">
<label for="resource2">Quiz Resource</label>
</div>
<div>
<input type="radio" id="resource3" name="resource" value="resource3">
<label for="resource3">Assignment Resource</label>
</div>
<br>
<button type="submit">Select Resource</button>
</form>
</div>
</body>
</html>
Create app/Views/lti/auto_submit.php
:
<!DOCTYPE html>
<html>
<head>
<title>Submitting to LMS...</title>
<script>
window.onload = function() {
document.getElementById('form').submit();
}
</script>
</head>
<body>
<form id="form" action="<?= $return_url ?>" method="post">
<input type="hidden" name="JWT" value="<?= $jwt ?>">
<p>Submitting response to LMS. If you are not automatically redirected, please click the button below.</p>
<button type="submit">Continue to LMS</button>
</form>
</body>
</html>
Create app/Views/lti/resource.php
:
<!DOCTYPE html>
<html>
<head>
<title>LTI Resource</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.card {
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
table, th, td {
border: 1px solid #ddd;
}
th, td {
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
</style>
</head>
<body>
<h1>LTI Resource</h1>
<div class="card">
<h2>Launch Information</h2>
<p>Message Type: <?= $launchData['https://purl.imsglobal.org/spec/lti/claim/message_type'] ?></p>
<p>Launch ID: <?= $launch->getLaunchId() ?></p>
<p>User: <?= $launchData['name'] ?? 'Anonymous' ?></p>
<p>Email: <?= $launchData['email'] ?? 'N/A' ?></p>
</div>
<?php if ($hasNrps && !empty($members)): ?>
<div class="card">
<h2>Class Roster</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Roles</th>
<?php if ($hasAgs): ?>
<th>Actions</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($members as $member): ?>
<tr>
<td><?= $member['name'] ?? 'Unknown' ?></td>
<td><?= $member['email'] ?? 'N/A' ?></td>
<td><?= implode(', ', $member['roles'] ?? []) ?></td>
<?php if ($hasAgs): ?>
<td>
<button onclick="submitGrade('<?= $member['user_id'] ?>', 85)">Submit Grade</button>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php if ($hasAgs): ?>
<div class="card">
<h2>Grades</h2>
<button onclick="getGrades()">View All Grades</button>
<div id="grades-container"></div>
</div>
<script>
function submitGrade(userId, score) {
fetch('<?= site_url('lti/resource/submit-grade') ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
user_id: userId,
score: score
})
})
.then(response => response.json())
.then(data => {
alert(data.success ? 'Grade submitted successfully!' : 'Error submitting grade');
});
}
function getGrades(userId = null) {
let url = '<?= site_url('lti/resource/get-grades') ?>';
if (userId) {
url += '?user_id=' + userId;
}
fetch(url)
.then(response => response.json())
.then(data => {
if (data.success) {
let html = '<h3>Retrieved Grades</h3><pre>' +
JSON.stringify(data.grades, null, 2) + '</pre>';
document.getElementById('grades-container').innerHTML = html;
} else {
alert('Error retrieving grades');
}
});
}
</script>
<?php endif; ?>
</body>
</html>
Add these routes to your app/Config/Routes.php
:
// LTI routes
$routes->get('lti/login', 'LtiLoginController::index');
$routes->post('lti/launch', 'LtiLaunchController::index');
$routes->get('lti/deep-link', 'LtiDeepLinkController::index');
$routes->post('lti/deep-link/response', 'LtiDeepLinkController::response');
$routes->get('lti/resource', 'LtiResourceController::index');
$routes->post('lti/resource/submit-grade', 'LtiResourceController::submitGrade');
$routes->get('lti/resource/get-grades', 'LtiResourceController::getGrades');
// JWKS endpoint
$routes->get('.well-known/jwks.json', 'JwksController::index');
<?php
namespace App\Controllers;
use App\Models\IssuerModel;
use App\Models\DeploymentModel;
use CodeIgniter\Controller;
class LtiAdminController extends Controller
{
protected $issuerModel;
protected $deploymentModel;
public function __construct()
{
$this->issuerModel = new IssuerModel();
$this->deploymentModel = new DeploymentModel();
}
public function index()
{
$issuers = $this->issuerModel->findAll();
return view('lti/admin/index', [
'issuers' => $issuers
]);
}
public function createIssuer()
{
if ($this->request->getMethod() === 'post') {
$this->issuerModel->insert([
'issuer' => $this->request->getPost('issuer'),
'client_id' => $this->request->getPost('client_id'),
'key_set_url' => $this->request->getPost('key_set_url'),
'auth_token_url' => $this->request->getPost('auth_token_url'),
'auth_login_url' => $this->request->getPost('auth_login_url'),
'auth_server' => $this->request->getPost('auth_server'),
'tool_private_key' => $this->request->getPost('tool_private_key'),
'kid' => $this->request->getPost('kid'),
]);
return redirect()->to('lti/admin')->with('message', 'Issuer created successfully');
}
return view('lti/admin/create_issuer');
}
public function viewDeployments($issuerId)
{
$issuer = $this->issuerModel->find($issuerId);
$deployments = $this->deploymentModel->where('issuer_id', $issuerId)->findAll();
return view('lti/admin/deployments', [
'issuer' => $issuer,
'deployments' => $deployments
]);
}
public function createDeployment($issuerId)
{
if ($this->request->getMethod() === 'post') {
$this->deploymentModel->insert([
'issuer_id' => $issuerId,
'deployment_id' => $this->request->getPost('deployment_id')
]);
return redirect()->to('lti/admin/issuer/'.$issuerId.'/deployments')
->with('message', 'Deployment created successfully');
}
$issuer = $this->issuerModel->find($issuerId);
return view('lti/admin/create_deployment', [
'issuer' => $issuer
]);
}
public function deleteIssuer($issuerId)
{
$this->issuerModel->delete($issuerId);
return redirect()->to('lti/admin')->with('message', 'Issuer deleted successfully');
}
public function deleteDeployment($deploymentId)
{
$deployment = $this->deploymentModel->find($deploymentId);
$issuerId = $deployment['issuer_id'];
$this->deploymentModel->delete($deploymentId);
return redirect()->to('lti/admin/issuer/'.$issuerId.'/deployments')
->with('message', 'Deployment deleted successfully');
}
}
Add routes for the admin controller:
// LTI Admin routes
$routes->group('lti/admin', ['filter' => 'auth'], function($routes) {
$routes->get('/', 'LtiAdminController::index');
$routes->get('issuer/create', 'LtiAdminController::createIssuer');
$routes->post('issuer/create', 'LtiAdminController::createIssuer');
$routes->get('issuer/(:num)/deployments', 'LtiAdminController::viewDeployments/$1');
$routes->get('issuer/(:num)/deployment/create', 'LtiAdminController::createDeployment/$1');
$routes->post('issuer/(:num)/deployment/create', 'LtiAdminController::createDeployment/$1');
$routes->get('issuer/(:num)/delete', 'LtiAdminController::deleteIssuer/$1');
$routes->get('deployment/(:num)/delete', 'LtiAdminController::deleteDeployment/$1');
});
-
Create the database tables using the migration:
php spark migrate
-
Generate a private/public key pair:
openssl genrsa -out private.key 2048 openssl rsa -in private.key -pubout -out public.key
-
Configure your LMS with:
- Login URL:
https://your-domain.com/lti/login
- Launch URL:
https://your-domain.com/lti/launch
- JWKS URL:
https://your-domain.com/.well-known/jwks.json
- Login URL:
-
Create an issuer and deployment in your admin panel.
-
Test the LTI launch from your LMS.