The Learning Tools Interoperability (LTI®) protocol is a standard for integration of rich learning applications within educational environments. ref
This library implements a tool provider as an Express server, with preconfigured routes and methods that manage the LTI® 1.3 protocol for you. Making it fast and simple to create a working learning tool with access to every LTI® service, without having to worry about manually implementing any of the security and validation required to do so.
This library is a fork of the original work by CVM Costa. The original library can be found here.
The library is configured to run with a PostgreSQL database, using TypeORM to manage database entities.
You will need to run PostgreSQL as a service on your device or using Docker.
TypeORM supports more than just PostgreSQL! You can modify this library and the entities it uses via the exported
register()
function, by specifying the{ entities: [] }
value explicitly. If overriding the standard entities, you will need to construct your own entity definitions and ensure that they meet the requirements of the library.
Setting up LTI-TypeScript
import { register } from 'lti-typescript';
async function main() {
const provider = await register(
'LTIKEY', // Encryption key used to sign cookies and tokens
{
type: 'postgres',
url: 'postgres://user:password@localhost:5432/database',
// See TypeORM documentation for more options: https://typeorm.io/docs/data-source/data-source-options
},
{
appRoute: '/', // Optionally, specify some of the reserved routes
loginRoute: '/login', // Optionally, specify some of the reserved routes
cookies: {
secure: false, // Set secure to true if the testing platform is in a different domain and https is being used
sameSite: 'none' // Set sameSite to 'None' if the testing platform is in a different domain and https is being used
},
devMode: true // Set DevMode to false if running in a production environment with https
// The full list of Provider options is defined by the "ProviderOptions" type.
}
)
// Set lti launch callback
provider.onConnect((token, req, res) => {
console.log(token)
return res.send('It\'s alive!')
});
// Deploy server and open connection to the database
await provider.deploy({ port: 3000 }); // Specifying port. Defaults to 3000
// Register platform
await provider.registerPlatform({
url: 'https://platform.url',
name: 'Platform Name',
clientId: 'TOOLCLIENTID',
authenticationEndpoint: 'https://platform.url/auth',
accesstokenEndpoint: 'https://platform.url/token',
authConfig: { method: 'JWK_SET', key: 'https://platform.url/keyset' }
});
}
Static, application-wide Database class which is initialized in Provider.setup()
Proxy method used to instantiate a Provider object and call setup(…) with the passed parameters.
The LTI-TypeScript Provider Class implements the LTI® 1.3 protocol and services.
Express server instance.
Type: Express
GradeService Class, implementing the Assignment and Grade service of the LTI® 1.3 protocol.
Type: GradeService
DeepLinkingService Class, implementing the Deep Linking service of the LTI® 1.3 protocol.
Type: DeepLinkingService
NamesAndRolesService Class, implementing the Names and Roles Provisioning service of the LTI® 1.3 protocol.
Type: NamesAndRolesService
Method used to setup and configure the LTI® provider. Additionally, initializes connection to the configured database.
port: 3000
silent: false
serverless: false
Starts listening to a given (if specified) port for LTI® requests.
serverless: true
to use LTI-TypeScript as a middleware.true
when the server starts listening.Closes connection to database and stops server.
Sets the callback method called whenever theres a sucessfull connection, exposing a token object containing the decoded idToken and the usual Express route parameters (Request, Response and Next).
provider.onConnect(async (token: IdToken, req: express.Request, res: express.Request, next: express.NextFunction) => {
return res.send(token)
});
// Equivalent to onConnect usage above
provider.app.get(provider.appRoute(), async (req: express.Request, res: express.Request, next: express.NextFunction) => {
return res.send(res.locals.token)
});
Sets the callback method called whenever theres a sucessfull deep linking request connection, exposing a token object containing the decoded idToken and the usual Express route parameters (Request, Response and Next). Through this callback you can display your Deep Linking view.
provider.onDeepLinking(async (token: IdToken, req: express.Request, res: express.Request, next: express.NextFunction) => {
return res.send(token)
});
Sets the callback method called when no valid session is found during a request validation.
provider.onSessionTimeout(async (req: express.Request, res: express.Request, next: express.NextFunction) => {
return res.status(401).send(res.locals.err)
});
LTI-TypeScript provides a default method for this callback.
Sets the callback method called when the token received fails the validation process.
provider.onInvalidToken(async (req: express.Request, res: express.Request, next: express.NextFunction) => {
return res.status(401).send(res.locals.err)
});
LTI-TypeScript provides a default method for this callback.
Sets the callback function called when the Platform attempting to login is not registered.
provider.onUnregisteredPlatform((req: express.Request, res: express.Request, next: express.NextFunction) => {
return res.status(400).send({ status: 400, error: 'Bad Request', details: { message: 'Unregistered Platform!' } })
});
LTI-Typescript provides a default method for this callback.
Sets the callback function called when the Platform attempting to login is not activated.
provider.onInactivePlatform((req: express.Request, res: express.Request, next: express.NextFunction) => {
return res.status(401).send({ status: 401, error: 'Unauthorized', details: { message: 'Platform not active!' } })
});
LTI-TypeScript provides a default method for this callback.
Gets the main application Route that will receive the final decoded Idtoken.
provider.appRoute;
Gets the login Route responsible for dealing with the OIDC login flow.
provider.loginRoute;
Gets the public JWK keyset Route.
provider.keysetRoute;
Gets the dynamic registration Route.
provider.dynRegRoute
Returns the list of whitelisted routes
Whitelists routes to bypass the LTI-TypeScript authentication protocol. If validation fails, these routes are still accessed but aren't given an identity token.
provider.whitelist = [...]
.provider.whitelist = [...provider.whitelist, <new routes>]
// Whitelisting routes
provider.whitelist('/log', '/home');
// Whitelisting routes with specific methods
provider.whitelist(...provider.whitelist, '/log', '/home', { route: '/route', method: 'POST' });
Registers a new Platform and returns a promise resolving to the new platform instance.
await provider.registerPlatform({
url: 'https://platform.url',
name: 'Platform Name',
clientId: 'TOOLCLIENTID',
authenticationEndpoint: 'https://platform.url/auth',
accesstokenEndpoint: 'https://platform.url/token',
authConfig: { method: 'JWK_SET', key: 'https://platform.url/keyset' }
});
Retrieves a Platform (if exists) with the given URL and client ID.
const platform = await provider.getPlatform('https://platform.url', 'TOOLCLIENTID');
Retrieves a Platform (if exists) whose kid
matches the provided platformId
.
const platform = await provider.getPlatformById('asdih1k12poihalkja52');
Deletes a Platform (if exists) with the given URL and client ID.
await provider.deletePlatform('https://platform.url', 'TOOLCLIENTID');
Deletes a Platform (if exists) whose kid
matches the passed platformId
.
await provider.deletePlatformById('60b1fce753c875193d71b');
Gets all platforms whose URL matches the passed URL.
const platforms = await provider.getPlatforms('http://platform.url');
Gets all platforms.
const platforms = await provider.getAllPlatforms();
Redirects to a new location. Passes Ltik if present.
provider.redirect(res, '/path', { newResource: true, query: { param: 'value' } })
// Redirects to /path?param=value
When using LTI-TypeScript, the first step must always be to call the Provider.setup(...)
method OR the register(...)
method to configure the LTI® provider:
// Require Ltijs package
import { register } from 'lti-typescript';
const provider = await register(
'LTIKEY', // Encryption key used to sign cookies and tokens
{
type: 'postgres',
url: 'postgres://user:password@localhost:5432/database',
// See TypeORM documentation for more options: https://typeorm.io/docs/data-source/data-source-options
},
{
appRoute: '/', // Optionally, specify some of the reserved routes
loginRoute: '/login', // Optionally, specify some of the reserved routes
cookies: {
secure: false, // Set secure to true if the testing platform is in a different domain and https is being used
sameSite: 'none' // Set sameSite to 'None' if the testing platform is in a different domain and https is being used
},
devMode: true // Set DevMode to false if running in a production environment with https
// The full list of Provider options is defined by the "ProviderOptions" type.
}
);
import { Provider } from 'lti-typescript';
const provider = new Provider();
provider.setup(
'LTIKEY', // Encryption key used to sign cookies and tokens
{
type: 'postgres',
url: 'postgres://user:password@localhost:5432/database',
// See TypeORM documentation for more options: https://typeorm.io/docs/data-source/data-source-options
},
{
appRoute: '/', // Optionally, specify some of the reserved routes
loginRoute: '/login', // Optionally, specify some of the reserved routes
cookies: {
secure: false, // Set secure to true if the testing platform is in a different domain and https is being used
sameSite: 'none' // Set sameSite to 'None' if the testing platform is in a different domain and https is being used
},
devMode: true // Set DevMode to false if running in a production environment with https
// The full list of Provider options is defined by the "ProviderOptions" type.
}
);
This method receives three arguments: encryption key, database options and provider options:
The encryptionKey
parameter is a string that will be used as a secret to sign the cookies set by LTI-TypeScript and encrypt some of the database information, such as access tokens and private keys.
The second parameter of the setup method, databaseOptions, is an object which satisfies the TypeORM DataSourceOptions
type.
The third parameter, providerOptions, is an optional parameter that handles the additional provider configuration:
Through the options parameter you can specify the routes for the reserved endpoints used by LTI-TypeScript:
appRoute
(or appUrl
) - Route used to handle successful launch requests through the onConnect
callback. Default: '/'.
loginRoute
(or loginUrl
) - Route used to handle the initial OIDC login flow. Default: '/login'.
keySetRoute
(or keySetUrl
) - Route used serve the tool's JWK keyset. Default: '/keys'.
dynRegRoute
(or dynRegUrl
) - Route used to handle Dynamic Registration requests. Default: '/register'.
{
...
appRoute: '/app',// Scpecifying main app route
loginRoute: '/loginroute', // Specifying login route
keysetRoute: '/keyset', // Specifying keyset route
dynRegRoute: '/register' // Specifying Dynamic registration route
...
}
LTI-TypeScript sets session cookies throughout the LTI® validation process, how these cookies are set can be configured through the cookies
field of the providerOptions parameter:
secure - Determines if the cookie can only be sent through https. Default: false.
sameSite - Determines if the cookie can be sent cross domain. Default: Lax.
domain - Determines the cookie domain. This option can be used to set cookies that can be shared between all subdomains.
{
...
cookies: { // Cookie configuration
secure: true,
sameSite: 'None',
domain: '.domain.com'
},
...
}
If the platform and tool are in different domains, some browsers will not allow cookies to be set unless they have the secure: true
and sameSite: 'None'
flags. If you are in a development environment and cannot set secure cookies (over https), consider using LTI-TypeScript in Development mode
.
LTI-TypeScript relies on cookies for part of the validation process, but in some development environments, cookies might not be able to be set, for instance if you are trying to set cross domain cookies over an insecure http connection.
In situations like this you can set the devMode
field as true and LTI-TypeScript will stop trying to validate the cookies and will instead use the information obtained through the ltik
token to retrieve the correct context information.
{
...
devMode: true, // Using development mode
...
}
DevMode should never be used in a production environment, and it should not be necessary, since most of the cookie issues can be solved by using the secure: true
and sameSite: None
flags.
See more about request authentication.
As part of the LTI® 1.3 protocol validation steps, LTI-TypeScript checks the idtoken's iat
claim and flags the token as invalid if it is older than 10 seconds.
This limit can be configured (or removed) through the tokenMaxAge
field:
tokenMaxAge
- Sets the idToken max age allowed in seconds. If false, disables max age validation. Default: 10.{
...
tokenMaxAge: 60, // Setting maximum token age as 60 seconds
...
}
Through the serverAddon
field you can setup a method that will be executed on the moment of the server creation.
This method will receive the Express
app as a parameter and so it can be used to register middlewares or change server configuration:
const middleware = (app) => {
app.use(async (req, res, next) => {
console.log('Middleware works!')
next() // Passing to next handler
});
}
const provider = new Provider();
provider.setup(
<encryptionKey>,
<databaseOptions>,
{
...
serverAddon: middleware // Setting addon method
...
},
);
Registered middlewares need to call
next()
, otherwise no further handlers will be reached.
Express
allows us to specify a path from where static files will be served.
LTI-TypeScript can use this functionality by setting the staticPath parameter of the constructor's additional options.
{
...
staticPath: path.join(__dirname, 'public'), // Setting static path
...
}
The specified path is internally bound to the root route:
app.use('/', express.static(SPECIFIED_PATH, { index: '_' }))
Accessing the files:
http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/images/bg.png
http://localhost:3000/hello.html
This can also be achieved and further customized by using server addons:
// Creating middleware registration
const middleware = (app) => {
app.use('/static', express.static(__dirname + '/public'));
}
//Configure provider
const provider = new Provider();
provider.setup(
<encryptionKey>,
<databaseOptions>,
{
...
serverAddon: middleware // Setting addon method
...
},
);
And then accessing the files through the specified /static
route:
http://localhost:3000/static/images/kitten.jpg
http://localhost:3000/static/css/style.css
http://localhost:3000/static/js/app.js
http://localhost:3000/static/images/bg.png
http://localhost:3000/static/hello.html
LTI-TypeScript Express
instance is configured to accept cross origin requests by default, this can be disabled by setting the cors
field to false:
{
...
cors: false, // Disabling cors
...
}
After the register()
or provider.setup()
methods are called, the returned provider
object gives you access to various functionalities to help you create your LTI® Provider.
The Provider is not a singleton class, and you can instantiate multiple instances of LTI-TypeScript across different ports (or middlewares) if needed.
Provider instances will need to be tracked to ensure resources can be closed properly and preventing unnecessary duplicate connections.
The provider.app
object is an instance of the underlying Express
server, through this object you can create routes just like you would when using regular Express.
provider.app.get('/route', async (req,res,next) => {
return res.send('It works!')
});
LTI-TypeScript reserved endpoint routes can be retrieved by using the following methods:
const appRoute = provider.appRoute; // returns '/' by default
const loginRoute = provider.loginRoute; // returns '/login' by default
const keySetRoute = provider.keySetRoute; // returns '/keys' by default
const dynRegRoute = provider.dynRegRoute; // returns '/register' by default
LTI-TypeScript allows you to configure it's main behaviours through callbacks:
The onConnect
callback is called whenever a successful launch request arrives at the main app url. This callback can be set through the provider.onConnect()
method.
The callback route will be given a first parameter token
, that is the user's validated idtoken, and the three Express route parameters (request, response and next).
The idtoken can also be found in the response.locals.token object.
provider.onConnect(async (token, req, res, next) => {
console.log(token)
return res.send('User connected!')
});
provider.onConnect()
is optional, you can simply create a route receiving requests at the appRoute
:// Equivalent to onConnect usage above
provider.app.get(provider.appRoute(), async (req, res, next) => {
console.log(res.locals.token)
return res.send('User connected!')
});
Launches directed at other endpoints are also valid but are not handled by the onConnect
callback, instead they must be handled by their own Express
route:
// This route can handle launches to /endpoint
provider.app.get('/endpoint', async (req, res, next) => {
console.log(res.locals.token)
return res.send('User connected!')
});
The onDeepLinking
callback is called whenever a successfull deep linking request arrives at the main app url. This callback can be set through the provider.onDeepLinking()
method.
The callback route will be given a first parameter token
, that is the user's validated idtoken, and the three Express route parameters (request, response and next).
This callback should be used to display your LTI® provider's deep linking UI.
provider.onDeepLinking(async (token, req, res, next) => {
return res.send('Deep Linking is working!')
});
The onInvalidToken
callback is called whenever the idtoken received fails the LTI® validation process. This callback can be set through the provider.onInvalidToken()
method.
The callback route will be given the three Express route parameters (request, response and next). And will also have access to a res.locals.err
object, containing information about the error.
This callback should be used to display your invalid token error screen.
provider.onInvalidToken(async (req, res, next) => {
return res.status(401).send(res.locals.err)
});
res.locals.err
object:{ status: 401, error: 'Unauthorized', message: 'ERROR_MESSAGE' }
The onSessionTimeout
callback is called whenever no valid session is found during a request validation. This callback can be set through the provider.onSessionTimeout()
method.
The callback route will be given the three Express route parameters (request, response and next). And will also have access to a res.locals.err
object, containing information about the error.
This callback should be used to display your session timeout error screen.
provider.onSessionTimeout(async (req, res, next) => {
return res.status(401).send(res.locals.err)
});
res.locals.err
object:{ status: 401, error: 'Unauthorized', message: 'ERROR_MESSAGE' }
The onUnregisteredPlatform
callback is called whenever the Platform attempting to start a LTI launch is not registered.
The callback route will be given the two Express route parameters (request, response).
This callback should be used to display your Unregistered Platform error screen.
provider.onUnregisteredPlatform((req, res) => {
return res.status(400).send({ status: 400, error: 'Bad Request', message: 'Unregistered Platform!' })
})
{ status: 400, error: 'Bad Request', message: 'UNREGISTERED_PLATFORM' }
The onInactivePlatform
callback is called whenever the Platform attempting to start a LTI launch was registered through Dynamic Registration and is not active.
The callback route will be given the two Express route parameters (request, response).
This callback should be used to display your Inactive Platform error screen.
provider.onInactivePlatform((req, res) => {
return res.status(401).send({ status: 401, error: 'Unauthorized', message: 'Platform not active!' })
});
{ status: 401, error: 'Unauthorized', message: 'PLATFORM_NOT_ACTIVATED' }
Deploying the application opens a connection to the configured database and starts the Express server.
await provider.deploy()
The provider.deploy()
method accepts an options
object with the following fields:
port
: The port which the underlying HTTP/S server will listen on. Default is 3000
.silent
: Whether or not the server will emit messages on startup and during activity. Default is false
.serverless
: Prevents an HTTP/S server from being deployed. Allows LTI-TypeScript to be used as a middleware in another application. Default is false
.await provider.deploy({ port: 3030, silent: false })
You can use LTI-TypeScript as a middleware by calling the deploy method with the serverless flag set to true.
const app = express()
const provider = new Provider();
await provider.setup(...);
// Start LTI provider in serverless mode
await provider.deploy({ serverless: true })
// Mount LTI-TypeScript express app into preexisting express app with /lti prefix
app.use('/lti', provider.app)
Platform manipulation methods require a connection to the database, so they can only be used after the provider.deploy()
method.
A LTI® tool works in conjunction with an LTI® ready platform, so in order for a platform to display your tool's resource, it needs to first be registered in the tool provider.
The provider.registerPlatform()
method returns a Promise that resolves the created Platform object.
let plat = await provider.registerPlatform({
url: 'https://platform.url',
clientId: 'TOOLCLIENTID',
name: 'Platform Name',
authenticationEndpoint: 'https://platform.url/auth',
accesstokenEndpoint: 'https://platform.url/token',
authToken: { method: 'JWK_SET', key: 'https://platform.url/keyset' }
});
platformUrl
: the platform's base URL.
clientId
: the client ID for the tool provided by the platform.
name
: Platform nickname.
authenticationEndpoint
: the platform's authentication endpoint.
accesstokenEndpoint
: the platform's access token request endpoint.
authToken
: the platform's authentication method and key (or keyset URL).
e.g., If the platform uses a JWK keyset:
authToken: { method: 'JWK_SET', key: 'https://platform.url/keyset' }
authToken: {
method: 'JWK_KEY',
key: `{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", "y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","kid":"keyid"}`,
}
authToken: {
method: 'RSA_KEY',
key: `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0
FPqri0cb2JZfXJ/DgYSF6vUpwmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/
3j+skZ6UtW+5u09lHNsj6tQ51s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQAB
-----END PUBLIC KEY-----`
}
Platforms can also be registered by utilizing the Dynamic Registration Service.
Registered platforms can be retrieved using the following methods:
provider.getPlatform(platformUrl: string, clientId: string)
The provider.getPlatform()
method receives two arguments, platformUrl
and clientId
, and returns a Promise that resolves a Platform object.
const platform = await provider.getPlatform('http://platform.url', 'CLIENTID') // Returns Platform object
provider.getPlatforms(platformUrl: string)
The provider.getPlatforms()
method receives one argument, platformUrl
, and returns a Promise that resolves to an array of Platform objects which match the provided URL.
const platforms = await provider.getPlatforms('http://platform.url'); // Returns Platform array
provider.getPlatformById(platformId: string)
The provider.getPlatformById()
method receives the platformId
and returns a Promise that resolves a Platform object.
const platform = await provider.getPlatformById('60b1fce753c875193d71'); // Returns Platform object
The platform ID can be found through the Platform.kid
method or in the platformId field of the idToken
object after a successful launch.
provider.getAllPlatforms()
The provider.getAllPlatforms()
method returns a Promise that resolves an Array containing every registered Platform.
const platforms = await provider.getAllPlatforms(); // Returns every registered platform
After a platform is registered, it's name, authenticationEndpoint, accesstokenEndpoint and authConfig parameters can still be modified in two ways:
Using Platform object:
The Platform object gives you methods to retrieve and modify platform configuration.
If the platform is already registered and you pass different values for the parameters when calling the provider.registerPlatform()
method, the configuration of the registered platform will be updated.
Note that the
platformUrl
andclientId
are used to identify collisions, and thus cannot be changed.
const platform = await provider.registerPlatform({
platformUrl: 'https://platform.url',
clientId: 'TOOLCLIENTID',
name: 'Platform Name 2', // Changing the name of already registered platform
});
Registered platforms can be deleted using the provider.deletePlatform()
and provider.deletePlatformById()
methods.
The provider.deletePlatform(platformUrl: string, clientId: string)
method receives two arguments, platformUrl
and clientId
:
await provider.deletePlatform('http://platform.url', 'CLIENTID') // Deletes a platform
The provider.deletePlatformById(platformId: string)
method receives the argument platformId
:
await provider.deletePlatformById('60b1fce753c875193d71b') // Deletes a platform
The LTI® 1.3 protocol works in such a way that every successful launch from the platform to the tool generates an IdToken that the tool uses to retrieve information about the user and the general context of the launch.
Whenever a successful launch request is received by LTI-TypeScript, the idToken received at the end of the process is validaded according to the LTI® 1.3 security specifications.
The valid idtoken is then separated into two parts idtoken
and contexttoken
, that are stored in the databased and passed along to the next route handler inside the response.locals
object:
The idtoken
will contain the platform and user information that is context independent, and will be stored in the res.locals.token
object, or the token
parameter if the onConnect
is being used:
provider.onConnect(async (token: IdToken, req: express.Request, res: express.Response) => {
// Retrieving idtoken through response object
console.log(res.locals.token)
// Retrieving idtoken through onConnect token parameter
console.log(token)
})
The idtoken
object is of the type IdToken
and consists of:
{
iss: "http://localhost/moodle",
clientId: "CLIENTID",
deploymentId: "1",
platformId: "60b1fce753c875193d71b611e895f03d",
platformContext: ContextProperties,
platformInfo: {
product_family_code: "moodle",
version: "2020042400",
guid: "localhost",
name: "moodle",
description: "Local Moodle"
},
user: "2",
userInfo: {
given_name: "Admin",
family_name: "User",
name: "Admin User",
email: "local@moodle.com",
},
}
The contexttoken
will contain the context specific information, and will be stored in the res.locals.context
object and as a part of the idtoken
object as the platformContext
field:
provider.onConnect(async (token, req, res) => {
// Retrieving contexttoken through response object
console.log(res.locals.context)
// Retrieving contexttoken through idtoken object
console.log(token.platformContext)
})
The contexttoken
object consists of:
// Example contexttoken for a Moodle platform
{
contextId: "http%3A%2F%2Flocalhost%2FmoodlewTtQU3zWHvVeCUf12_57",
path: "/",
user: "2",
roles: [
"http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator",
"http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor",
"http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator"
],
targetLinkUri: "http://localhost:3000",
context: {
id: "2",
label: "course",
title: "Course",
type: [
"CourseSection"
]
},
resource: {
title: "LTI-TypeScript Demo",
id: "57"
},
custom: {
"system_setting_url": "http://localhost/moodle/mod/lti/services.php/tool/1/custom",
"context_setting_url": "http://localhost/moodle/mod/lti/services.php/CourseSection/2/bindings/tool/1/custom",
"link_setting_url": "http://localhost/moodle/mod/lti/services.php/links/{link_id}/custom"
},
lis: {
"person_sourcedid": "",
"course_section_sourcedid": ""
},
endpoint: {
scope: [
"https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
"https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly",
"https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
"https://purl.imsglobal.org/spec/lti-ags/scope/score"
],
lineitems: "http://localhost/moodle/mod/lti/services.php/2/lineitems?type_id=1",
lineitem: "http://localhost/moodle/mod/lti/services.php/2/lineitems/26/lineitem?type_id=1"
},
namesRoles: {
context_memberships_url: "http://localhost/moodle/mod/lti/services.php/CourseSection/2/bindings/1/memberships",
service_versions: [
"1.0",
"2.0"
]
},
launchPresentation: {
locale: "en",
document_target: "iframe",
return_url: "http://localhost/moodle/mod/lti/return.php?course=2&launch_container=3&instanceid=57&sesskey=6b5H1MF8yp"
},
messageType: "LtiResourceLinkRequest",
version: "1.3.0"
}
LTI-TypeScript need as way to retrieve the correct idtoken
and contexttoken
information whenever a tool makes a request. The authentication protocol is based on two items, a session cookie and a ltik token.
A platform can launch to any of the tool's endpoints, but only launches targeting the specified appRoute
will be sent to the onConnect callback. Launches to other endpoints must be handled by their specific Express
routes.
At the end of a successful launch, LTI-TypeScript redirects the request to the desired endpoint, but it also does two other things:
Sets a signed session cookie containing the platformCode
and userId
information;
Sends a ltik JWT token containing the same platform and user information, with additional context information as a query parameter to the endpoint.
Whenever the tool receives a request not directed at one of the reserved endpoints it attempts to validate the request by matching the information received through the session cookie with the information contained in the ltik token.
The ltik
token MUST be passed to the provider through either the query parameters, body parameters or an Authorization header (Bearer or LTIK-AUTH-V1).
https://tool-url.com?ltik=<ltik>
Authorization: Bearer <ltik>
Authorization: LTIK-AUTH-V1 Token=<ltik>, Additional=<additional authorization, e.g., Bearer token>
LTIK-AUTH-V1
authorization schema, req.headers.authorization
will only include the Additional
portion of the header. The ltik
token can be found in req.token
.LTI-TypeScript will look for the ltik
in the following order:
LTIK-AUTH-V1 Authorization
query
body
Bearer Authorization
In the case of requests coming from different subdomains, usually it is necessary to set
mode: cors
andcredentials: 'include'
flags to include the cookies in the request.If for some reason the cookies could not be set in your development environment, the usage of the devMode flag eliminates the validation step that matches the cookie information, instead using only the information contained in the ltik token.
If the validation fails, the request is handled by the invalidTokenCallback or the sessionTimeoutCallback.
Routes can be whitelisted to bypass the LTI-TypeScript authentication protocol in case of validation failure, this means that these routes work normally, but if the request sent to them fails validation they are still reached but don't have access to a idtoken
or contexttoken
.
A good way to exemplify this behaviour is by using it to create a landing page, that will be accessed if a request to the whitelisted route fails validation:
// Whitelisting the main app route and /landingpage to create a landing page
provider.whitelist = [provider.appRoute(), { route: '/landingpage', method: 'get' }];
// When receiving successful LTI® launch redirects to app, otherwise redirects to landing page
provider.onConnect(async (token, req, res, next) => {
// Checking if received idtoken
if (token) return res.sendFile(path.join(__dirname, './public/index.html'));
else provider.redirect(res, '/landingpage'); // Redirects to landing page
});
Whitelisted routes are defined using the provider.whitelist
setter that can receive an array of either string
s or RouteType
object.
Route strings will be whitelisted for every method:
provider.whitelist = ['/route1', '/route2', '/route3'];
RouteType objects allow you to specify whitelisted methods:
provider.whitelist = [{ route: '/route1', method: 'get' }];
// Route objects can also be whitelisted for every method
provider.whitelist = [{ route: '/route1', method: 'all' }];
// The provider.whitelist setter can receive different types simultaneously.
provider.whitelist = [{ route: '/route1', method: 'get' }, { route: '/route2', method: 'post' }, '/route3'];
Routes can also be set using Regex which means that you can whitelist a big range of routes:
// Using Regex
provider.whitelist = [{ route: new RegExp(/^\/route2/), method: 'get' }];
The new RegExp(/^\/route1/)
regex will whitelistd every route that starts with /route
.
Be careful when using regex to whitelist routes, you could whitelist routes accidentally and that can have a big impact on your application. It is recommended to use the start-of-string (^) and end-of-string ($) anchors to avoid accidental matches.
The LTI-TypeScript authentication protocol relies on the ltik
token being passed to endpoints as query parameters.
To make this process seamless, the provider.redirect()
method can be used to redirect to an endpoint passing the ltik
token automatically:
provider.onConnect(async (token, req, res) => {
return provider.redirect(res, '/route'); // Redirects to /route with the ltik token
});
provider.get('/route', async (req, res) => {
return provider.redirect(res, '/route/b?test=123'); // Redirects to /route/b with the ltik token and additional query parameters
});
The provider.redirect()
method requires two parameters:
Response - The Express
response object, that will be used to perform the redirection and retrieve the idtoken
and ltik
.
URL - The redirection target in the form of a string.
The url
parameter can be an internal route ('/route') or a complete URL ('https://domain.com'), but how the redirection works depends on some conditions:
If the target is on the same domain and subdomain ('https://domain.com', '/route'), it will have access to the ltik
and session cookie
and will pass the LTI-TypeScript authentication protocol.
If the complete URL is on the same domain but on a different subdomain ('https://a.domain.com'), it will have access to the ltik
but it might require a cookie domain to be set:
// Setup provider example
await provider.setup(
<encryptionKey>,
<databaseOptions>,
{
...
cookies: { // Cookie configuration
secure: true,
sameSite: 'None',
domain: '.domain.com'
},
...
},
);
Setting the domain to .domain.com
allows the session cookie
to be accessed on every domain.com subdomain (a.domain.com, b.domain.com).
ltik
, but it will not have access to a session cookie
, and will only be able to make successful requests to whitelisted routes.idtoken
(whitelisted route), provider.redirect()
method will still perform the redirection, but the target will not have access to the ltik
nor the session cookie
.The provider.redirect()
method also has an options
parameter that accepts two fields:
newResource
: If this field is set to true, the contexttoken
object has it's path
field changed to reflect the target route. The path
field can be used to keep track of the main resource route even after several redirections.provider.onConnect(async (token, req, res) => {
return provider.redirect(res, '/route', { newResource: true })
});
query
: This field can be used to easely add query parameter to target URL.provider.onConnect(async (token, req, res) => {
return provider.redirect(res, '/path', { newResource: true, query: { param: 'value' } })
// Redirects to /path?param=value
});
If for some reason you want to redirect manually, the
ltik
token can be retrieved, after a valid request, through theres.locals.ltik
variable.
The Deep Linking Service class documentation can be accessed here.
The Assignment and Grades Service class documentation can be accessed here.
The Names and Roles Provisioning Service class documentation can be accessed here.
The Dynamic Registration Service documentation can be accessed here.
{ debug: true }
within the Provider options.Learning Tools Interoperability® (LTI®) is a trademark of the IMS Global Learning Consortium, Inc. (https://www.imsglobal.org)
This library is a derivative work of CVM Costa's original LTIJS library.