The Canvas App Complete Connection Library (CACCL) is an all-in-one library for building Canvas-integrated apps. By handling LTI, authorization, and API for you, CACCL makes building Canvas-integrated tools quick and easy.
Beta: this project is in beta and breaking changes may occur at any time.
The fastest way to get set up is to use our template app. It uses Node.js and Express.js on the back-end and React on the front-end. Both use typescript. If you'd like to set up your project manually, check out the Manual Setup section.
Make a new npm project and navigate to the top-level directory.
Then, use npx create-caccl
and follow instructions.
Among many other updates, you should now find two folders: server/
and client/
which contain an Express server and a React app, respectively.
Out of the box, your CACCL app will be ready to run in development mode. Here's how to set up your server so it is ready for production.
In server/src/index.ts
, you'll find a call to initCACCL({ ... })
. All CACCL configuration gets passed into initCACCL
.
All CACCL apps integrate with Canvas via LTI v1.1 (LTI v1.3 coming soon). You can think of LTI as a standardized interface between Canvas and an app that integrates with Canvas. For more info on LTI, check out the IMS Global LTI Docs.
When your LTI app is installed into Canvas, it will be installed with installation credentials: consumer key and consumer secret (also referred to as a shared secret).
Provide your installation credential(s) to CACCL as an lti.installationCredentials
map in the form { key => secret }
.
initCACCL({
lti: {
installationCredentials: {
'first-key': 'first-secret',
'second-key': 'second-secret',
...
},
},
});
If you only have one key/secret pair (all app installs use the same installation credentials), you can include your credentials as two environment variables instead: CONSUMER_KEY
and CONSUMER_SECRET
.
Disable Authorization After Launch: If the api is configured (see the next section) but you would not like to automatically authorize users with the API when they launch via LTI, add the lti.dontAuthorizeAfterLaunch: true
flag. This is not a feature that's used very often, but it is useful if you sometimes want users to be redirected for API authorization (perhaps you only use the API for specific features or specific users). Then, since the user will not automatically be authorized to use the API, you'll need to manually redirect the user to the authorization process (see the "redirectToAuth" function in later sections). Note: the lti.dontAuthorizeAfterLaunch
flag is automatically set to true if the app is not set up for API integration.
initCACCL({
lti: {
dontAuthorizeAfterLaunch: true,
},
});
Self Launch: Usually, LTI apps are launched by users who start in Canvas. However, if you'd like your app to be able to launch itself, CACCL has a clever self-launch process that allows your app to launch itself, given information about the Canvas instance and course. To enable this feature, add an lti.selfLaunch
configuration object. CACCL needs to know the appId (also referred to as the external tool id) of your app as it is installed in the course that you'd like to self-launch from. For more information, see the Enable Self Launch section. There are many ways you can share appIds with CACCL:
You can skip this section if your app doesn't need to access the Canvas API on behalf of the current user. If your app uses a predefined access token (not recommended), check out the Access API via Predefined Access Token section.
To integrate with the Canvas API, your app needs to be registered with the Canvas instance. This is something you'll need to work through with the school/university/organization and their Canvas admins. Once they approve your app and add it to their Canvas instance, you should be able to get developer credentials for the app (client id and secret).
Provide your developer credential(s) to CACCL as an auth.developerCredentials
map in the form { canvasHost => { clientId, clientSecret } }
.
initCACCL({
auth: {
developerCredentials: {
'canvas.harvard.edu': {
clientId: '12340000000000000000125',
clientSecret: 'dgha7ht29837hgasdhfa0873grasheklh287gt097a08h3ug8',
},
},
},
});
If you only have one set of developer credentials to include, you can include your credentials as three environment variables: DEFAULT_CANVAS_HOST
, CLIENT_ID
, and CLIENT_SECRET
.
If your client-side React app accesses the Canvas API directly, you can skip this section. But if your client app does not need access to the Canvas API, you can disable it by adding the following flag:
initCACCL({
api: {
disableClientSideAPI: true,
},
});
Sometimes, universities and schools limit the API scopes that apps are allowed to access. If your app is limited, you will need to include an array of scopes when initializing CACCL, included as api.scopes
:
initCACCL({
api: {
scopes: [
'url:GET|/api/v1/courses',
'url:GET|/api/v1/courses/:course_id/assignments',
...
],
},
});
First, you'll need access to "sandbox course" in a real Canvas instance. You can visit canvas.instructure.com to log in or create an account. A "sandbox course" can be any Canvas course that is used for testing. We recommend adding one test teacher, at least one test TA, and at least one test student to your sandbox.
Get each test user's access token (teacher, TAs, students):
- Log in as the user
- Click "Account" in the top left
- Click "Settings"
- Scroll down to "+ Access Token"
- Follow instructions
From the top level of your project, create a /config/devEnvironment.json
file and add your sandbox information:
{
"canvasHost": "canvas.harvard.edu",
"courseId": 19248,
"teacherAccessToken": "1234~fasdhf782egjoasidnfga8723rhfahs9d8ga7yegf",
"taAccessTokens": [
"1234~ncb6dhf0qe9gga6q3b48vb87df8adf787w"
],
"studentAccessTokens": [
"1234~3r5983tbtnfm28tn2898ansd928377t097",
"1234~riout8r9e8u7y38f7a78odg8g7rgh87aer"
]
}
canvasHost
is the hostname of the Canvas instance containing your sandbox. This is optional and defaults to canvas.instructure.com
.
courseId
can be found in the URL when visiting the Canvas course: https://canvas.harvard.edu/courses/19248
where 19248
is the id.
teacherAccessToken
is the access token for a teacher in the sandbox.
taAccessTokens
is an optional array of access tokens for TAs in the sandbox.
studentAccessTokens
is an optional array of access tokens for students in the sandbox.
If your app has any custom parameters that you want to include, add them in a customParams
map:
{
...
"customParams": {
"name": "value"
...
}
}
Remember that custom parameters cannot have capital letters. For example, termName
is not allowed and should be replaced with simply term
or term_name
.
Also, we support the case where your app doesn't get launched via the /
path. This is an unusual case, but nevertheless, it is supported by caccl
. To include custom launch paths, simply include them in a list:
{
...
customLaunchPaths: [
{
"name": "Cat Video Player",
"path": "/videos/cats"
},
{
"name": "Desserts Video Player",
"path": "/videos/desserts"
}
],
};
You can now head over to the Using CACCL section.
Check out the Advanced Setup section for more advanced configuration.
Now that your CACCL app is configured, you're ready to start development!
Open three terminal windows/tabs and navigate to the top-level directory of the project.
In the first window, use npm run dev:server
to initialize the server in dev mode.
In the second window, use npm run dev:client
to initialize the client in dev mode.
In the third window, use npm run dev:canvas
to initialize a Canvas launch simulator.
Follow instructions in the third window. Logs from the server will appear in the first window and logs from the client will appear in the browser console.
To check the user's current status, use CACCL's getStatus
function to get a status object.
If the user has not launched via LTI, the status object will take the form:
{
launched: false,
}
If the user has launched via LTI, the status object will take the form:
{
launched: true,
launchInfo: LaunchInfo,
authorized: boolean,
}
Where authorized
is true if the user is authorized to access the Canvas API and launchInfo
contains all LTI launch info. See the LaunchInfo docs for detailed information on all the properties in the launchInfo
object.
Import getStatus
:
import { getStatus } from 'caccl/server';
From within a route, call getStatus
with the express req
instance:
const status = await getStatus(req);
Import getStatus
:
import { getStatus } from 'caccl/client';
From anywhere in the client, call getStatus
:
const status = await getStatus();
First, make sure the user is authorized to access the Canvas API. See the section above to check if status.authorized
is true
. If the user is not authorized, consider settinglti.authorizeAfterLaunch
to true
so users are automatically authorized when they launch. Otherwise, you can manually trigger the authorization process.
Import getAPI
:
import { getAPI } from 'caccl/server';
From within a route, call getAPI
with the express req
instance to get an instance of the api:
const api = await getAPI({
req,
});
Use api
to interact with the Canvas API. Check out the CACCL API Docs for more info on all the powerful CACCL API functions that make it super easy to work with the Canvas API. Note: the default value for courseId
will be the id of the course that the user launched from. Example:
const students = await api.course.listStudents();
Import getAPI
:
import { getAPI } from 'caccl/client';
Anywhere on the client, call getAPI
to get an instance of the api:
const api = await getAPI();
Use api
to interact with the Canvas API. Check out the CACCL API Docs for more info on all the powerful CACCL API functions that make it super easy to work with the Canvas API. Note: the default value for courseId
will be the id of the course that the user launched from. Example:
const students = await api.course.listStudents();
If you want to set a default number of times that requests are retried, include numRetries
(if excluded, numRetries
is 3):
// Server
const api = await getAPI({
res,
numRetries: 5,
});
// Client
const api = await getAPI({
numRetries: 5,
});
If you want to set a default number of items per page, include itemsPerPage
(if excluded, itemsPerPage
is 100):
// Server
const api = await getAPI({
res,
itemsPerPage: 50,
});
// Client
const api = await getAPI({
itemsPerPage: 50,
});
You can mix and match all of these additional options.
If you set lti.authorizeAfterLaunch
to true
, your user should be authorized for Canvas API access as soon as they launch via LTI, so this section will probably be irrelevant to you.
If you don't always want users to be authorized for Canvas API access (maybe they only need it for advanced features, for example), you can leave out the lti.authorizeAfterLaunch
flag and manually redirect the user to the authorization process whenever you need API access.
Import getStatus
:
import { redirectToAuth } from 'caccl/server';
From within a route, call redirectToAuth
with the express req
instance:
redirectToAuth(res);
Import redirectToAuth
:
import { redirectToAuth } from 'caccl/client';
From anywhere in the client, call redirectToAuth
:
redirectToAuth();
If your users launch your app via Canvas, you don't need to enable self launches and you can skip this section.
If you enabled self launch, you can trigger a self-launch which redirects the user through Canvas for an LTI launch. This is useful if the user starts in your app instead of starting in Canvas.
Import redirectToSelfLaunch
:
import { redirectToSelfLaunch } from 'caccl/server';
From within a route, call redirectToSelfLaunch
:
redirectToSelfLaunch({
res,
courseId: 123875,
});
Import redirectToSelfLaunch
:
import { redirectToSelfLaunch } from 'caccl/client';
From anywhere on the client, call redirectToSelfLaunch
:
redirectToSelfLaunch({
courseId: 123875,
});
If the Canvas hostname is not the default self-launch value, you can manually include it as canvasHost
:
// Server
redirectToSelfLaunch({
res,
courseId: 123875,
canvasHost: 'canvas.harvard.edu',
});
// Client
redirectToSelfLaunch({
courseId: 123875,
canvasHost: 'canvas.harvard.edu',
});
If you already know the appId and you don't want CACCL to look up the appId using the maps and/or admin access token, you can include the id as appId
:
// Server
redirectToSelfLaunch({
res,
courseId: 123875,
appId: 58394,
});
// Client
redirectToSelfLaunch({
courseId: 123875,
appId: 58394,
});
If you'd like to store some state in the launchInfo
object (helpful for resuming tasks or otherwise supporting continuity), you can include the state as a JSONifiable object as selfLaunchState
. After the self-launch, you'll find this state in launchInfo.selfLaunchState
.
// Server
redirectToSelfLaunch({
res,
courseId: 123875,
selfLaunchState: {
nextPage: '/discussions',
},
});
// Client
redirectToSelfLaunch({
courseId: 123875,
selfLaunchState: {
nextPage: '/discussions',
},
});
You can mix and match all of these additional options.
If your app is a custom assignment and it finishes by passing grades back to Canvas, you can use CACCL's handlePassback
function to send feedback/points/etc. to Canvas.
Import handlePassback
:
import { handlePassback } from 'caccl/server';
From within a route, call handlePassback
with the express req
instance:
// Text submission
handlePassback({
req,
text: 'Text of the submission',
});
// URL submission
handlePassback({
req,
url: 'https://url.of/the/submission',
});
To add a grade, either include score
(raw point value) or percent
(percent of total allowed points):
// Include score
handlePassback({
req,
...
score: 24,
});
// Include percent
handlePassback({
req,
...
percent: 97,
});
To overwrite the submission timestamp (defaults to now), include either a string (ISO 8601) or JS Date instance as submittedAt
:
handlePassback({
req,
...
submittedAt: subDateObj,
});
Import handlePassback
:
import { handlePassback } from 'caccl/client';
From anywhere on the client, call handlePassback
:
// Text submission
handlePassback({
text: 'Text of the submission',
});
// URL submission
handlePassback({
url: 'https://url.of/the/submission',
});
To add a grade, either include score
(raw point value) or percent
(percent of total allowed points):
// Include score
handlePassback({
...
score: 24,
});
// Include percent
handlePassback({
...
percent: 97,
});
To overwrite the submission timestamp (defaults to now), include either a string (ISO 8601) or JS Date instance as submittedAt
:
handlePassback({
...
submittedAt: subDateObj,
});
You're welcome to use another request sender, but if you use ours, it'll provide more consistency and will ensure your requests work with the development environment.
Import sendRequest
:
import { sendRequest } from 'caccl/server';
See below on how to use sendRequest.
Import sendRequest
:
import { sendRequest } from 'caccl/client';
See below on how to use sendRequest.
To send requests, simply call sendRequest
with the path
of the endpoint:
const response = await sendRequest({
path: '/api/boards',
});
The response is an object of the form { body, status, headers }
where body
is the JSON data returned from the endpoint, status
is the http status code, and headers
is a map of headers in the response.
If the hostname is not the same as the current server, include it as host
:
const response = await sendRequest({
path: '/api/boards',
host: 'another.host.com',
});
If the method is not GET
, you can include it as method
:
const response = await sendRequest({
path: '/api/boards',
method: 'POST',
});
Independent of the method of request, you can include an object containing parameters. These are sent as either the body or query (depending on the method) and each value is stringified. If you need to send a JSON object as a parameter value, JSON.stringify
it and then JSON.parse
it on the other end. Include parameters as params
:
const response = await sendRequest({
path: '/api/boards',
method: 'POST',
params: {
title: 'My Board',
position: 10,
},
});
There are also a few advanced options:
- Include
headers
, a map{ name => value }
of headers to include with the request - Include
numRetries
to allow the request sender to retry the request this number of times
Installation credentials are created by you. We recommend using a powerful random string generator to generate consumer secrets, and we recommend using a combination of random text and descriptive text for consumer keys. Example:
consumerKey = 'clientname-248915tjds8f';
consumerSecret = 'fja8web7g9a9s8mue8t2b-3t7n-98asdn7f8v6as5dv76fb8a67sdtfsd-gne8g';
You can either pass these values into the initializer function on the server, or you can add them as environment variables (CONSUMER_KEY
and CONSUMER_SECRET
).
Developer credentials are created by whoever manages the Canvas instance that you're trying to integrate with. For example, if the instance for a university, try reaching out to their central IT department or Canvas admin team. If you are a 3rd party company, be prepared for your client to require a contract and security review at this point. Once you reach the appropriate person, ask them to generate a new "Developer Key" for your app, following the How do I add a developer key for an account? instructions. Note: your Redirect URI
should be https://<AppHostName>/canvas/launch
.
To build your app, simply run npm run build
from the top-level directory of the app. You might want to run npm install
before building, just to make sure all dependencies are all installed.
Once your app is built, simply run npm run start
from the top-level directory of the app.
To install your app into a course, you'll need to create an LTI v1.1 (LTI v1.3 coming soon) configuration cartridge (XML). There are great tools online for building these XML files. Try googling "LTI XML Generator" or just use the edu-apps xml generator. Set the launch URL as https://<AppHostName>/canvas/launch
.
Once you have the cartridge, install your LTI app using these steps:
- Open the Canvas course or account that you'd like to install the app into
- Click "Settings" in the left-hand navigation menu
- Switch to the "Apps" tab
- Click "View App Configurations"
- Click "+ App"
- Set "Configuration Type" to "Paste XML"
- Add an appropriate App Name
- Set "Consumer Key" to one of your consumer keys
- Set "Shared Secret" to the associated consumer secret
- Paste your cartridge in the "XML Configuration" box
- Click "Submit"
Note: it's convenient to upload this XML cartridge to your app's server. Then, you can use the "By URL" configuration type and just paste the URL instead of the full XML contents.
By default, CACCL sets up an express server for you. However, if you want more control, you have two options:
Either include an express
configuration object when initializing CACCL or use environment variables to customize. All parameters are optional: you can include any params and exclude any params as well. You can also mix and match between parameters and environment variables.
Description | Config Param | Environment Variable | Default |
---|---|---|---|
Port to listen to | port | PORT | 8080 |
Session secret | sessionSecret | SESSION_SECRET | random |
Cookie name | cookieName | COOKIE_NAME | random |
Session duration (min) | sessionMins | SESSION_MINS | 360 |
Example:
initCACCL({
express: {
cookieName: 'My App',
sessionDuration: 60,
},
});
You can also include a custom express-session store as express.sessionStore
. By default, we use a non-leaking memory store.
Finally, if there are any operations you'd like to perform on the express app after it is set up but before CACCL adds any routes to it, include an express.preprocessor
function.
If you'd like complete control over the express app configuration, create your own express app, make sure you add support for an express-session, and include it as express.app
.
initCACCL({
express: {
app: myCustomApp,
},
});
By default, when a user authorizes your app for API access, that authorization (packet containing token, expiration, refresh token) will be stored in memory. When your app is restarted or the memory is cleared, or when the user switches between multiple servers, their authorization will no longer be available and they will need to reauthorize the app.
To store API authorization longer term, provide CACCL with a token store initialization function that, when called, creates a token store. Include it when calling initCACCL
on the server: api.initTokenStore
. The initTokenStore function must implement our CACCLStore Initialization Function interface, which is a function that returns a CACCLStore instance.
Examples of such usage: create an initTokenStore
function that returns a CACCLStore instance where get
reads from a db and set
writes to a db.
Although we recommend using CACCL's oauth exchange process to authorize the user and then access the Canvas API on their behalf, sometimes you may already have an access token that is predefined. If this is the case, you can create your own instance of the CACCL API:
import initAPI from 'caccl-api';
const api = initAPI({
canvasHost: 'canvas.harvard.edu',
accessToken: '1234~fdjabv9w8efnlaijspo8egbaiushefw2etghkj',
});
You can also include advanced API configuration defaults:
Description | Config Param | Default |
---|---|---|
Number of times to retry failed requests | numRetries | 3 |
Default number of items to request per page | itemsPerPage | 100 |
Default courseId to use for requests | defaultCourseId | none |
Example:
const api = initAPI({
canvasHost: 'canvas.harvard.edu',
accessToken: '1234~fdjabv9w8efnlaijspo8egbaiushefw2etghkj',
numRetries: 5,
});
Use api
to interact with the Canvas API. Check out the CACCL API Docs for more info on all the powerful CACCL API functions that make it super easy to work with the Canvas API. Example:
const students = await api.course.listStudents();
Usually, LTI apps are launched by users who start in Canvas. However, if you'd like your app to be able to launch itself, CACCL has a clever self-launch process that allows your app to launch itself, given information about the Canvas instance and course. To enable this feature, add an lti.selfLaunch
configuration object. CACCL needs to know the appId (also referred to as the external tool id) of your app as it is installed in the course that you'd like to self-launch from. There are many ways you can share appIds with CACCL:
If an appId is used across an entire Canvas instance (there is only one appId for that Canvas instance), include it in the lti.selfLaunch.hostAppIdMap
map { canvasHost => appId }
:
initCACCL({
lti: {
selfLaunch: {
hostAppIdMap: {
'canvas.harvard.edu': 18934,
},
},
},
});
If an appId is used in a specific course in a Canvas instance, include it in the lti.selfLaunch.courseAppIdMap
map { canvasHost => courseId => appId }
:
initCACCL({
lti: {
selfLaunch: {
courseAppIdMap: {
'canvas.harvard.edu': {
124892: 84290,
124899: 92848,
},
},
},
},
});
If an appId is not known ahead of time, you can include Canvas admin access token(s) so your app can look up appIds on the fly. Include the tokens in the lti.selfLaunch.adminAccessTokenMap
map { canvasHost => accessToken[] }
:
initCACCL({
lti: {
selfLaunch: {
adminAccessTokenMap: {
'canvas.harvard.edu': [
'1423~jf8tu0jalsjdf0jgh8ha9w8uefljasdf8jrga',
'1423~f8g09jgha9shd31hg9hha89qwrpmzhdb8398h',
],
},
},
},
});
If you include lti.selfLaunch.adminAccessTokenMap
, CACCL will cache appIds in a store that implements our CACCLStore interface. We default to a memory-based token store, but you can provide your own store by including an lti.selfLaunch.initAppIdStore
function that implement our CACCLStore Initialization Function interface, which is a function that returns a CACCLStore instance. Pro tip: create your own custom store and pre-populate it with appIds (store keys take the form ${canvasHost}/${courseId}
and values are appIds).
initCACCL({
lti: {
selfLaunch: {
initAppIdStore: initMyMongoBasedStore,
},
},
});
If you have other runtime logic that determines the appropriate appId, you can also provide the appId when triggering a self-launch.
Finally, when performing a self-launch, you'll need both a courseId
and canvasHost
. If there's a canvasHost
that you use more often, you can include an lti.selfLaunch.defaultCanvasHost
so you don't have to include the canvasHost
if self-launching via that defaultCanvasHost
.
initCACCL({
lti: {
selfLaunch: {
defaultCanvasHost: 'canvas.harvard.edu',
},
},
});
If you'd like to set up your own project from scratch, or if you'd like to integrate CACCL with an existing app:
- Add
caccl
to your server and client withnpm install --save caccl
- On your server, initialize CACCL:
import initCACCL from 'caccl/server';
await initCACCL({
...
});
Then, you can follow other instructions on configuring CACCL on the server (see sections above).