-
Notifications
You must be signed in to change notification settings - Fork 40
Messages
The class library provides support for the following services:
-
basic-lti-launch-request/LtiResourceLinkRequest- standard launch request -
ContentItemSelectionRequest/LtiDeepLinkingRequest- deep linking (fka content-item selection) request -
ContentItemSelection/LtiDeepLinkingResponse- deep linking (fka content-item) response -
ToolProxyRegistrationRequest- LTI 2 tool proxy registration request -
DashboardRequest- an unofficial extension for handling portal-type content -
ConfigureLaunchRequest- an unoffcial extension for configuration requests
A common process for handling messages by a tool is implemented via the handleRequest method. A similar method is provided for platforms.
The primary use case for this library's classes is to validate an incoming launch request from a platform. Once a record has been initialised for the platform (see above), the verification of the authenticity of the LTI launch request is handled automatically by the Tool class. A sub-class is created and the onLaunch method overridden to define the code to be run when a valid launch request is received.
use ceLTIc\LTI;
class MyApp extends LTI\Tool {
function onLaunch() {
// Insert code here to handle incoming connections - use the user,
// context and resourceLink properties of the class instance
// to access the current user, context and resource link.
}
}
$tool = new MyApp($dataConnector);
$tool->handleRequest();
The handleRequest method checks the authenticity of the incoming request. For LTI 1.0/1.1/1.2/2.0 it does this by verifying:
- the OAuth signature (using the shared secret recorded for the platform),
- the timestamp is within a defined limit of the current time, and
- the nonce value has not been previously used.
For requests using LTI 1.3 a request is automatically sent to the platform's Authentication URL with JWT returned being verified using the platform's public key (either as recorded in the database or as retrieved from the JSON Web Key URL) and the current time.
Only if the request passes all the checks is the onLaunch method called (see below). The process also captures various standard launch parameters to allow access to services. By default, some leniency against the LTI specification is permitted; for example, some checks are case insensitive with parameters being returned in the correct case. If strict adherence to the LTI specification is required then the leniency which is applied by default can be disabled by setting the optional parameter to true:
$tool->handleRequest(true);
When a launch is not valid, the onError method is called but, by default, a message is returned to the platform with a more detailed reason to be logged (or displayed on the page if no return URL has been provided in the message). If the tool's debug mode is set then the more detailed reason for the failure is also displayed to the user. Debug mode can be set in your code; for example:
$tool = new MyApp($dataConnector);
$tool->debugMode = true;
$tool->handleRequest();
In addition a platform can trigger debug mode by including a custom parameter of debug=true (which will be passed to the tool with a name of custom_debug) in its launch message.
When debug mode is active more detailed messages are also sent to the PHP error log. These debug log messages can also be activated on a per-platform basis by saving a platform with its debug mode set; for example:
use ceLTIc\LTI;
$platform = LTI\Platform::fromConsumerKey('testing.edu', $dataConnector);
$platform->debugMode = true;
$platform->save();
This allows debug-level logging to be activated for specific platforms only.
The onLaunch method may be used to:
- create the user account if it does not already exist (or update it if it does);
- create any workspace required for the resource link if it does not already exist (or update it if it does);
- establish a new session for the user (or otherwise log the user into the tool application);
- keep a record of the return URL for the platform (for example, in a session variable);
- set the URL for the home page of the application so the user may be redirected to it.
Even though a request may be in accordance with the LTI specification, a tool may still choose to reject it because, for example, not all of the required data has been passed. A request may be rejected as follows:
- set the
okproperty tofalse; - optionally set an error message to return to the user (if the platform supports this facility).
For example:
function onLaunch() {
...
$this->ok = false;
$this->reason = 'Incomplete data in launch message';
}
If your tool also supports the Content-Item (Deep Linking) message or LTI 2 registration messages, then their associated method should also be overridden; for example:
use ceLTIc\LTI;
class MyApp extends LTI\Tool {
function onLaunch() {
// Insert code here to handle incoming launches - use the user, context
// and resourceLink properties to access the current user, context and resource link.
}
function onContentItem() {
// Insert code here to handle incoming content-item requests - use the user and context
// properties to access the current user and context.
}
function onRegister() {
// Insert code here to handle incoming registration requests - use the user
// property to access the current user.
}
function onError() {
// Insert code here to handle errors on incoming connections - do not expect
// the user, context and resourceLink properties to be populated but check the reason
// property for the cause of the error. Return TRUE if the error was fully
// handled by this method.
}
}
$tool = new MyApp($dataConnector);
$tool->handleRequest();
The onDashboard and onConfigure methods are provided to handle the unofficial DashboardRequest and ConfigureLaunchRequest message types.
When a tool processes an incoming message, it automatically looks up the details of the sending platform to allow it to verify its authenticity. When a tool need to send a message to a platform it must set up both an object to represent itself and one to represent the receiving platform. For example,
use ceLTIc\LTI;
$dataConnector = LTI\DataConnector\DataConnector::getDataConnector($db, DB_TABLENAME_PREFIX);
$tool = new MyApp(null);
$tool->rsaKey = $toolPublicKey; // Not required if a JKU is set
$tool->jku = 'https://tool.com/jwks';
$tool->platform = LTI\Platform::fromConsumerKey($consumerKey, $dataConnector);
$tool->platform->secret = $sharedSecret;
LTI\Tool::$defaultTool = $tool;
Since the consumer key and shared secret are not required if only LTI 1.3 is being used, the fromPlatformId method can be used to create the Platform object, for example:
$tool->platform = LTI\Platform::fromPlatformId($platformId, $clientId, $deploymentId, $dataConnector);
If the communication is being made during a session initiated by a message received from a platform, then an alternative solutuon would be to retain the ID of the platform in the user session as part of the processing of the message (see above); for example:
...
function onLaunch() {
...
$_SESSION['platform_pk'] = $this->platform->getRecordId();
...
}
...
$tool->platform = LTI\Platform::fromRecordId($_SESSION['platform_pk'], $dataConnector);
When sending a message from a tool to a platform with LTI 1.3, it is important to use the tool to sign the message; for example:
$formParams = LTI\Tool::$defaultTool->signParameters($_SESSION['return_url'], 'ContentItemSelection', $_SESSION['lti_version'], $formParams);
$page = LTI\Util::sendForm($_SESSION['return_url'], $formParams);
echo $page;
exit;
This is because LTI 1.3 uses asymmetric keys, so the private key of the tool must be used to sign the message, rather than the platform's key (for which only the public key will be known to the tool). With earlier versions of LTI, this did not matter as the same consumer key and secret were used for signing messages in both directions. Note that, where the name of message type has been changed in LTI 1.3, the original name is used within the library - the mapping to the new name occurs automatically. The platform object will be used to extract the platform ID, client ID and the deployment ID when preparing the message; its public key will also be used when the experimental content encryption is applied (see below).
One of the differences in handling a content-item/deep linking message request is that any LTI links your tool passes back to be created will not yet have an associated resource link ID. One solution to this is to create an internal resource link ID for the resource and add this as a custom parameter to the link with a name of content_item_id. When a launch request is received from a resource link ID which is not recognised and this custom parameter is present, a check is made for a resource link with the value of the parameter. If found, the resource link ID is updated with the resource link ID from the launch request and the custom parameter will be ignored on any subsequent launches. In this way, the resource created via a content-item request will be automatically connected to the resource link created in the platform. For example, here is some sample code based on this workflow implemented in the sample Rating application:
...
$item = new LTI\Content\Item('LtiLink');
$item->setMediaType(LTI\Content\Item::LTI_LINK_MEDIA_TYPE);
$item->setTitle($_SESSION['title']);
$item->setText($_SESSION['text']);
$item->icon = new LTI\Content\Image(getAppUrl() . 'images/icon50.png', 50, 50);
$item->custom = array('content_item_id' => $_SESSION['resource_id']);
$formParams['content_items'] = LTI\Content\Item::toJson($item);
if (!is_null($_SESSION['data'])) {
$formParams['data'] = $_SESSION['data'];
}
LTI\Tool::$defaultTool->platform = LTI\Platform::fromRecordId($_SESSION['platform_pk'], $dataConnector);
$formParams = LTI\Tool::$defaultTool->signParameters($_SESSION['return_url'], 'ContentItemSelection', $_SESSION['lti_version'], $formParams);
$page = LTI\Util::sendForm($_SESSION['return_url'], $formParams);
echo $page;
exit;
...
The $_SESSION['resource_id'] variable contains a GUID generated on launch; this is used as the placeholder until the first launch of this item is performed and the validation of the request will automatically replace this resource link ID with the one passed in the launch parameters.
The library can also be used by a platform as well as a tool. For example, a platform may need to verify a return message from a deep linking (content-item) workflow. Since LTI 1.3 uses asymmetric keys for signing and verifiying messages and service requests, a tool object must be initialised with its public key. This is not required for earlier versions of LTI as the same consumer key and secret is used by both parties.
use ceLTIc\LTI\Tool;
$tool = new MyApp(null);
$tool->rsaKey = $toolPublicKey; // Not required if a JKU is set
$tool->jku = 'https://tool.com/jwks';
Tool::$defaultTool = $tool;
$platform = Platform::fromConsumerKey($consumerKey, null);
$platform->secret = $sharedSecret;
$platform->platformId = $platformId;
$platform->clientId = $clientId;
$platform->deploymentId = $deploymentId;
$platform->signatureMethod = 'RSA256';
$platform->rsaKey = $platformPrivateKey;
$platform->handleRequest();
Note that it is not necessary to set the platform property of the tool object as the platform is controlling the process and it will automatically access the Tool::$defaultTool object for any tool properties required.
LTI 1.3 uses JWTs to pass data between platforms and tools. The library includes interfaces for the following open source PHP JWT libraries:
- PHP-JWT from Firebase
- JOSE library from Spomky-labs [now deprecated]
- JWT Framework from the Web-Token project by Spomky-Labs
Only the Firebase library is installed as a dependency of this library and is used by default. One of the alternative libraries can be installed and used by setting the default JWT interface, for example:
use ceLTIc\LTI\Jwt;
...
Jwt\Jwt::setJwtClient(new Jwt\SpomkyLabsClient());
or
Jwt\Jwt::setJwtClient(new Jwt\WebTokenClient());
These alternative libraries can be installed using Composer as follows:
composer require spomky-labs/jose
and
composer require web-token/jwt-core
composer require web-token/jwt-signature
composer require web-token/jwt-signature-algorithm-rsa
composer require web-token/jwt-key-mgmt
composer require web-token/jwt-checker
In addition, the following packages are required if you wish to use the experimental content encryption feature of this library:
composer require web-token/jwt-encryption
composer require web-token/jwt-encryption-algorithm-rsa
composer require web-token/jwt-encryption-algorithm-aescbc
A JWT library of your own choice can be used by writing a Jwt\ClientInterface for it using the existing ones as a guide.
The life of JWTs generated by the library can be changed from its default of 60 seconds by setting the life property, for example:
ceLTIc\LTI\Jwt\Jwt::$life = 30;
The leeway allowed for clock skew between the sending and receiving servers when verifying a JWT can be changed from its default value of 180 seconds by setting the leeway property, for example:
ceLTIc\LTI\Jwt\Jwt::$leeway = 60;
Note that the Firebase library does not support content encryption, so to use this experimental feature an alternative library must be used.
There is typically a one-to-one relationship between a link created in a platform and a resource in a tool. When the resurces offered by a tool are sharable items, such as content, this might be a many-to-one relationship, where many links in one or more platforms can be safely associated with the same resource in the tool. However, when the resources offered by a tool are not suitable for sharing (for example, a group activity) the relationship is likely to be restricted to one-to-one, with each link in the platform being associated with its own acitvity within the tool. But there may be times when an instructor would like to have students from more than one course or from more than one platform (i.e. from more than one link) to engage together in an activity. In such cases a tool could implement a mechanism by which an instructor identifies which links are to be grouped together and this mapping used to direct students into the shared activity, but this may require giving access to data from other platforms and having a convenient mechansim for identifying the links (which may have still to be created). Thus, this library implements a simple mechanism of share keys which can be used to enable users launching from multiple links to be directed into a single resource at the tool end. The process works as follows:
- One of the resource links is chosen to be the primary connection from which the sharing is controlled;
- An instructor launched from the primary link generates a share key;
- The instructor passes the share key to an instructor of a (secondary) link to be included in the shared activity;
- The instructor adds the share key to the custom parameters for their secondary link within the platforms;
- When the secondary link is launched, the library reports the user as having launched from the primary link and so a tool will automatically allow them to share the activity;
- A share key can only be used once; a new one should be generated for each link which is to be allowed to share the primary activity.
The secondary links may exist on different platforms provided they are all configured to use the same instance of the tool. When generating a share key, an instructor may choose to automatically approve the link which uses it, or not. In the latter case, all launches from the secondary link will fail until the instructor approves it; at least one launch will be required to activate the share arrangement before it can be approved.
A share key is of the form share_key=xyz, where xyz is the unique value generated. A share key value may have a length of up to 32 characters. The share key custom parameter is activated on the first launch from the secondary link; thereafter its value is ignored but its presence is required to indicate that the sharing option is still required.
When a sharing arrangement is in place, in the Tool's onLaunch method, the resoureLink property will refer to the primary link. The resourceLink property of the userResult object will refer to the link from which the user actually launched. It may be necessary to keep a record of both in your application.
An example implementation of share keys can be found in the sample LTI Rating application. Note that the mechanism does depend upon a platform allowing custom parameters to be specified for individual LTI links; thus, it cannot be used with platforms such as Canvas which do not currently support this.
The specifications relating to JWTs include a facility for encrypting the content of the JWT. Since the JWTs used by LTI messages pass through the user's browser, their content is open to view (just as the data being passed as POST parameters is when using earlier versions of LTI), so using encryption affords some useful protection. Whilst the LTI specification does not make use of content encryption, this library supports it as an experimental feature. If both the platform and the tool support encryption, then a standard LTI message JWT can be passed between them as an encrypted JWT (a JWE). The library uses the same keys for encrypting content as are used for signing the JWTs with the RSA-OAEP-256 key encryption algorithm, the DEF compression algorithm and one of the following key encryption algorithms:
- A128CBC-HS256
- A192CBC-HS384
- A256CBC-HS512
The keys of the recipient are used: the public key is used to encrypt the data, and the private key is used to decrypt the content. The content being passed in encrypted form is identical to the JWT which would be passed normally when encryption is not used; that is, a signed JWT for which the signature will also be verified (after decryption).
Apart from configuring a JwtClient which supports encryption, the only code change required is to set the encryptionMethod property of the signing party; for example:
LTI\Jwt\Jwt::setJwtClient(new LTI\Jwt\WebTokenClient());
...
$tool->encryptionMethod = 'A128CBC-HS256';
You can see the encryption process in operation in the [saLTIre|https://saltire.lti.app] test tool.
© 2025 Stephen P Vickers. All Rights Reserved.