Skip to content

Messages

Stephen Vickers edited this page Jul 31, 2023 · 9 revisions

The class library provides support for the following messages sent by platforms to tools:

  • basic-lti-launch-request/LtiResourceLinkRequest - standard launch request
  • ContentItemSelectionRequest/LtiDeepLinkingRequest - deep linking (fka content-item selection) request
  • ContentItemUpdateRequest/LtiDeepLinkingUpdateRequest - deep linking (fka content-item selection) request (note that this message is not formally part of the Deep Linking 2.0 specification but is included to enable equivalence between LTI 1.3 and earlier versions)
  • ToolProxyRegistrationRequest - LTI 2 tool proxy registration request
  • LtiStartProctoring - start a proctoring process
  • LtiEndAssessment - end a proctoring process
  • LtiSubmissionReviewRequest - submission review request
  • DashboardRequest - an unofficial extension for handling portal-type content
  • ConfigureLaunchRequest - an unoffcial extension for configuration requests
  • Authentication request - the mechanism used in LTI 1.3 to confirm that a message should be sent by a platform

and the following messages sent by tools to platforms:

  • ContentItemSelection/LtiDeepLinkingResponse - deep linking (fka content-item) response
  • LtiStartAssessment - start a proctored assessment
  • Initiate login request - the mechanism used in LTI 1.3 for establishing a message sequence

It also supports LTI 1.3 dynamic registration request messages received from a platform.

A common process for handling messages by a tool is implemented via the handleRequest method. A similar method is provided for platforms.

Validating a launch request

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 Initialising a platform section in Usage), 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

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 ok property to false;
  • 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';

}

The onAuthenticate method

The onAuthenticate method is called when a platform or tool is handling an LTI 1.3 authentication request message. The default platform implementation ensures that the original message is sent to the tool provided any login_hint and lti_message_hint parameters have their correct values. The default tool implementation will check the returned state and nonce values (including against those stored in the platform storage frame when used). By default a state has a life of 10 seconds but this can be altered as required; for example:

use ceLTIc\LTI\Tool;

Tool::$stateLife = 60;  // Allow 60 seconds for an authentication request

Content-item/deep linking and registration messages

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 LTI 2 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 dynamic registration of an LTI 1.3 tool by a Moodle platform is supported by the onRegistration method. A simple implementation of this method is included in the library but can be overridden by a tool to use a bespoke user interface (see the sample Rating application application for an example). The default implementation will process the request and display a result page; no user interaction is supported except for closing the result page which is displayed within the platform. The implementation in the Rating application provides a basic user interface comprising a Continue button to initiate the registration process, as well as options for automatically enabling the newly configured platform and limiting the period of time for which the platform is enabled. This illustrates how the standard process can be adapted to meet specific requirements:

public function doRegistration()
{
    $platformConfig = $this->getPlatformConfiguration();
    if ($this->ok) {
        $toolConfig = $this->getConfiguration($platformConfig);
        error_log(var_export(json_encode($toolConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), true));
        $registrationConfig = $this->sendRegistration($platformConfig, $toolConfig);
        if ($this->ok) {
            $platform = $this->getPlatformToRegister($platformConfig, $registrationConfig, false);
            if (defined('AUTO_ENABLE') && AUTO_ENABLE) {
                $platform->enabled = true;
            }
            if (defined('ENABLE_FOR_DAYS') && (ENABLE_FOR_DAYS > 0)) {
                $now = time();
                $platform->enableFrom = $now;
                $platform->enableUntil = $now + (ENABLE_FOR_DAYS * 24 * 60 * 60);
            }
            $this->ok = $platform->save();
            if (!$this->ok) {
                $this->reason = 'Sorry, an error occurred when saving the platform details.';
            }
        }
    }
}

The onDashboard and onConfigure methods are provided to handle the unofficial DashboardRequest and ConfigureLaunchRequest message types.

Strict LTI conformance checks

By default the library will allow certain discrepancies between the parameters passed and the LTI specifications. A strict check can be enforced via a parameter to the handleRequest method:

$strictMode = true;

$tool->handleRequest($strictMode);

Third-party cookies

Browsers are tightening their security rules over allowing third-party cookies. The library includes code which attempts to identify when a tool's cookies are being blocked and, when they are, an attempt is made to open the tool in a new window. This process may involve forcing the PHP session ID to revert to its original value (when the first attempt to process a message was made). When this occurs the onResetSessionId method is called in case the tool has any additional changes which need to be made at such a time (for example, if it has implemented a custom session handler).

Note that the third-party cookie check is only made when a relaunch is requested (LTI 1.0.1/1.1.2) or with LTI 1.3 messages; it will not be made during a standard OAuth-signed message. The check can be disabled via a parameter to the handleRequest method:

$strictMode = false;
$disableCookieCheck = true;

$tool->handleRequest($strictMode, $disableCookieCheck);

Warnings

If an error is detected during the processing of a message the cause is reported in the reason property (as well as the ok property being set to false). The reported error will be the first found. If a message includes additional errors, these may be reported as warnings via a parameter to the handleRequest method:

$strictMode = false;
$disableCookieCheck = false;
$generateWarnings = true;

$tool->handleRequest($strictMode, $disableCookieCheck, $generateWarnings);

Any warning messages will be included in the warnings array property (e.g. $tool->warnings).

If a message successfully passes its checks but strict mode is not enabled, then any checks which have been ignored will be reported as warnings if this feature has been enabled.

Communicating from a tool to a platform

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

Content-item/deep linking resource link IDs

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.

Communicating from a platform to a tool

The library can also be used by a platform as well as a tool.

Initialising the platform and tool

Since in this use case the library is being used in the implementation of a platform, the platform object is likely to be initialised with constant values as illustrated in the following examples.

LTI 1.0/1.1/1.2/2.0

$platform = new Platform();
$platform->setKey($consumerKey);
$platform->secret = $sharedSecret;
$platform->signatureMethod = 'HMAC-SHA1';

LTI 1.3

$platform = new Platform();
$platform->platformId = $platformId;
$platform->clientId = $clientId;
$platform->deploymentId = $deploymentId;
$platform->rsaKey = $platformPrivateKey;
$platform->signatureMethod = 'RS256';

A platform may connect to many different tools; when using the default data connector objects a tool may be uniquely identified its record ID; for example:

$tool = new Tool($dataConnector);
$tool->setKey($consumerKey);
$tool->secret = $sharedSecret;
$tool->messageUrl = 'https://tool.com/lti';
$tool->inititiateLoginUrl = 'https://tool.com/oidc';
$tool->rsaKey = $toolPublicKey;  // Or set a JKWS endpoint
$tool->jku = 'https://tool.com/jwks';
$tool->save();
$toolId = $tool->getRecordId();

Additional properties can added to a tool or platform as settings (see the getSetting and setSetting methods). In this way they their values will be saved with the object. If you wish to extend the class and add new properties, the their values can be saved and retreived from the settings by overriding the fixPlatformSettings and fixToolSettings methods of your data connector. For example:

use ceLTIc\LTI;

class APlatform extends LTI\Platform {

    public $code = null;

    public function __construct($dataConnector = null) {
        parent::__construct($dataConnector);
        $this->code = DEFAULT_CODE;
    }

}

class MyDataConnector extends LTI\DataConnector\DataConnector_mysqli {

    protected function fixPlatformSettings($platform, $isSave) {
        parent::fixPlatformSettings($tool, $isSave);
        if (!$isSave) {
            $platform->code = $platform->getSetting('__code');
            $platform->setSetting('__code');
        } else {
            $platform->setSetting('__code', $platform->code);
        }
    }

}

By convention, additional properties are given setting names with a prefix of a double underscore (__) to avoid any name clashing with normal settings and those added by the library (which are given a single underscore prefix).

Sending Messages

All messages comprise a signed set of parameters. The preparation of the parameters to be sent is outside the scope of this library, but the signing and sending of the message can be achieved as follows:

// Initialise message parameters to be sent; for example:
$messageParameters['user_id'] = $user->id;
$messageParameters['lis_person_name_full'] = $user->name;
$messageParameters['tool_consumer_instance_name'] = 'The University of Testing';
...

// Initialise the tool and platform (see Usage page)
$tool = Tool::fromRecordId($toolId, $dataConnector);
Tool::$defaultTool = $tool;

$html = $platform->sendMessage($tool->messageUrl, 'basic-lti-launch-request', $messageParameters);
echo $html;

Note that the message parameters can be defined in the same way, regardless of which version is being used to make the connection. The HTML returned by the sendMessage method will be an auto-submitted form containing the message parameters and signature. The lti_version and lti_message_type parameters are added automatically. In the case of an lTI 1.3 connection, the form will represent an initiate login request; the message parameters are, by default, saved in the user's PHP session pending the authentication request from the tool. This behaviour can be modified by overriding the onInitiateLogin and onAuthenticate methods; for example, a WordPress implementation could save the data as user options:

...

protected function onInitiateLogin(&$url, &$loginHint, &$ltiMessageHint, $params)
{
    $user = wp_get_current_user();
    $data = array(
        'login_hint' => $loginHint,
        'lti_message_hint' => $ltiMessageHint,
        'params' => $params
    );
    update_user_option($user->ID, 'lti-connect-login', $data);  // Save data
}

protected function onAuthenticate()
{
    $user = wp_get_current_user();
    $login = get_user_option('lti-connect-login');  // Retrieve saved data
    update_user_option($user->ID, 'lti-connect-login', null);  // Delete user option
    $parameters = Util::getRequestParameters();
    if ($parameters['login_hint'] !== $login['login_hint'] ||
        (isset($login['lti_message_hint']) && (!isset($parameters['lti_message_hint']) || ($parameters['lti_message_hint'] !== $login['lti_message_hint'])))) {
        $this->ok = false;
        $this->messageParameters['error'] = 'access_denied';
    } else {
        $this->messageParameters = $login['params'];
    }
}

...

Note that the onInitiateLogin method allows the values of the message URL, login hint and LTI message hint parameters to be overridden if required.

Receiving messages

When a platform received a message from a tool, such as in the completion of a deep linking (content-item) workflow, it must verify its authenticity by checking the signature. This will automatically use the consumer key and shared secret or, for LTI 1.3 connections, the tool's public key (or JWKS endpoint).

Tool::$defaultTool = $tool;
$platform->handleRequest();
if ($platform->ok) {
  ...
}

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.

JSON Web Tokens (JWTs)

LTI 1.3 uses JWTs to pass data between platforms and tools. The library includes interfaces for the following open source PHP JWT libraries:

Only the Firebase library is installed as a dependency of this library and is used by default. An alternative library can be installed and used by setting the default JWT interface, for example:

Jwt\Jwt::setJwtClient(new Jwt\WebTokenClient());

The alternative library can be installed using Composer as follows:

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

Alternatively, the entire library can be installed as follows:

composer require web-token/jwt-framework

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.

Share keys

There is typically a one-to-one relationship between a link created in a platform and a resource in a tool. When the resources 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 activity 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 mechanism 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:

  1. One of the resource links is chosen to be the primary connection from which the sharing is controlled;
  2. An instructor launched from the primary link generates a share key;
  3. The instructor passes the share key to an instructor of a (secondary) link to be included in the shared activity;
  4. The instructor adds the share key to the custom parameters for their secondary link within the platforms;
  5. 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;
  6. 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.

Content encryption

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.