How to Add LTI® 1.3 to Your Application
The normative reference throughout is the LTI® 1.3 specification from 1EdTech Consortium, Inc., with the implementation guide as the more readable companion.
Making an application launchable from a learning management system means implementing the tool side of LTI® 1.3: receiving a launch, verifying it, and turning it into a signed-in user in your own system.
This page covers what that involves. It is worth being direct about one thing
first, because a lot of the material that ranks for this question is not:
much of the published guidance on adding LTI® to an application describes
LTI® 1.1, which 1EdTech deprecated in 2022. If a tutorial
tells you to sign requests with OAuth 1.0a or to publish an XML configuration
with a blti:launch_url, it is teaching a specification you should not build
against.
What you are actually building
Four things, in order of how much trouble they cause.
1. A login initiation endpoint
The platform posts here to start a launch. You reply by redirecting the browser
to the platform's authentication endpoint, echoing back the hints you were sent
and adding a state and a nonce of your own.
The work is small. The catch is that you must be able to look up which
registration this is from the iss and client_id in the request, before you
have verified anything, and you must store the state and nonce somewhere the
next request can find them.
2. A launch endpoint that verifies a JWT
The platform posts the ID Token here. Before trusting anything in it you must check:
- the signature, against the platform's key set, selecting the
key by the
kidin the token header issandaudmatch the registrationexphas not passed- the
noncehas not been seen before - the
statematches the one you issued - the message type and version are ones you support
Each of these is a real check with a real failure mode. Skipping the nonce means
a captured launch can be replayed. Skipping aud means a token minted for a
different tool is accepted. Fetching the key set on every launch without caching
will get you rate-limited; caching it forever means a key rotation takes you
offline.
3. An account model that can absorb a launched user
The launch tells you a user exists. It does not tell you who they are in your system.
Identify them by the platform issuer and the sub claim together. sub is
unique within a platform, not globally, so two institutions can legitimately
send the same value. Then decide what happens on first launch: provision an
account silently, or send them somewhere to link an existing one.
Personal data may not arrive at all. Platforms can be configured to send no name and no email, so an integration that requires an email address to create an account will fail at some institutions. Treat identity as the issuer and subject; treat everything else as optional.
4. A registration process you can hand to a stranger
An administrator at each institution has to register you. That means giving them your login initiation URL, your redirect URLs, your key set URL, and a target link URI, and receiving back an issuer, a client ID and a deployment ID.
This is where most integrations actually fail, and rarely for interesting reasons: a trailing slash present in one field and not another, a client ID with an invisible character in it, a redirect URL that does not exactly match the URL being launched.
If you want the services too
Launching is the minimum. Most tools want at least one of the LTI® Advantage services, and each is a separate implementation:
| To do this | You implement |
|---|---|
| Read the course roster | Names and Role Provisioning |
| Create gradebook columns and post scores | Assignment and Grade Services |
| Let a teacher pick content to embed | Deep Linking |
They share an authentication model that is separate from the launch: you sign a JWT assertion with your private key, exchange it at the platform's token endpoint for an OAuth 2.0 access token, cache that token until it expires, and call the service with it. So before writing any service code you need a signing key, a key set to publish, and a token cache.
Availability is per course. A platform can grant grade access in one course and not another, so check what the ID Token reports for this launch rather than assuming.
The part nobody warns you about
Everything above is the specification. The remaining work is that platforms implement it differently.
Canvas sends an issuer that is not the URL you would expect. Blackboard shares one registration across many institutions and distinguishes them by deployment ID. Some platforms send no personal data by default; some send it always. Some support dynamic registration and some require every field to be entered by hand. Some grant services per course, some per installation.
None of this is in the specification, and all of it is in your support queue. Our LMS setup guides document the per-platform differences we have had to handle.
The alternative
The other option is to not implement the protocol. A service performs the handshake, verifies the token, manages the keys, and hands your application the result.
With LTIAAS the tool side reduces to receiving a redirect and making one authenticated request:
// LTIAAS completes the launch and redirects the browser to your URL with an
// ltik appended. Read it server-side on that first request.
app.get('/launch', async (req, res) => {
const ltik = req.query.ltik
const idtoken = await axios.get('https://your.ltiaas.com/api/idtoken', {
headers: { Authorization: `LTIK-AUTH-V2 ${API_KEY}:${ltik}` }
})
// Already verified. Identify the user by platform plus subject.
const externalId = `${idtoken.data.platform.id}:${idtoken.data.user.id}`
const user = await db.users.upsert({ externalId, ...profileFrom(idtoken.data) })
req.session.userId = user.id
res.redirect('/app')
})
There is no JWT verification in that code, no key set fetching, no nonce store, and no OAuth client credentials flow, because none of it is yours to do. Reading a roster or posting a grade is likewise a REST call rather than a service implementation.
Receiving your first launch is the end-to-end version of this with the account setup included, and LTI® 1.3 as a service is the honest comparison of the two approaches.
Either way, do this
Whichever route you take:
- Build against LTI® 1.3. Adding 1.1 to a new integration means adopting a deprecated security model for platforms that mostly also speak 1.3. If a specific institution genuinely cannot, LTIAAS can enable 1.1 for your account rather than you maintaining a second implementation.
- Test against a real LMS early. Free Moodle sandboxes exist, and the difference between the specification and a live platform is where the time goes.
- Do not require an email address. Some platforms will never send one.
- Support deep linking sooner than feels necessary. A tool that can only be added as one undifferentiated link is a worse product inside a course.
Common questions
How long does it take to add LTI® to an application?
Implementing the protocol yourself is usually measured in weeks, and the per-platform differences continue after that. Using a service, a first working launch is typically the same day, because the work reduces to one redirect and one API call.
Do I need to be certified by 1EdTech?
No. Certification is optional and separate from working. Some institutions ask for it in procurement, so it is worth knowing whether the tools you depend on hold it.
Do I need a different integration for each LMS?
No. The protocol is identical across platforms, so the code is written once. What differs per platform is registration — where an administrator enters the values, and which services that platform grants.
Can I test LTI® without access to an LMS?
Yes. 1EdTech publishes a reference implementation for testing, and free Moodle sandboxes are available. Testing against at least one real LMS before launch is still worthwhile, because platforms differ in what they send.
Next
- What is LTI® 1.3? — the launch sequence in detail
- LTI® 1.3 as a service — build or buy
- Receiving your first launch — the LTIAAS walkthrough
