The LTI® 1.3 Security Model
An LTI® launch arrives as an HTTP request from a browser, claiming to come from a university. Everything in it is attacker-controllable until something proves otherwise. This is what does the proving, and which parts of it are easy to get wrong.
The rules come from the LTI® 1.3 specification and 1EdTech's security framework, built on JSON Web Key and OAuth 2.0.
What the model has to defend against
Four things, and they are worth naming because each maps to a specific check:
- Forgery. Anyone can POST to your launch endpoint claiming to be an institution's LMS.
- Replay. A launch captured once — from a log, a proxy, a shared machine — should not work a second time.
- Misdirection. A token minted for one tool should not be accepted by another.
- Staleness. A launch from last month should not still open a session.
How it defends against them
Forgery: signatures over shared secrets
The platform signs the ID Token with a private key it never
discloses. The tool verifies with the matching public key, fetched from the
platform's published key set. The token header names a kid,
which selects the key.
This is the change from LTI® 1.1, where both sides held the same secret. A shared secret has to be transported to be shared, is rarely rotated because rotating needs both sides to act together, and if leaked lets an attacker mint launches against every tool it was shared with. A public key is worth nothing to an attacker.
Two ways this goes wrong in practice: fetching the key set over a connection you do not validate, and caching a key set forever so a legitimate rotation looks like an attack — or never caching, and being rate-limited into an outage.
Replay: nonce and state
Every launch carries a nonce. The tool records it and rejects a repeat. The
window only has to cover the token's lifetime, but within that window a
duplicate has to fail.
state is separate and does a different job: the tool generates it, sends it in
the authentication request, and checks it matches on the way back. That is what
ties the response to a request this tool actually made, rather than one an
attacker initiated.
Both are easy to skip because nothing appears broken when you do. A launch without nonce checking works perfectly, right up until someone replays one.
Misdirection: the audience claim
aud names the client the token was minted for. A tool that does not check it
will happily accept a token issued for a different tool by the same platform.
iss likewise has to match the registration the tool thinks it is handling — and
because a tool may hold many registrations, "match the registration" means
looking up by issuer and client ID together, not just recognising the issuer.
Staleness: expiry
exp is short by design. Check it, and do not add tolerance beyond a small
allowance for clock skew.
Clock skew is worth its own note because it produces a confusing failure: if the platform's server clock has drifted forward, a token can look as though it was issued in the future and be rejected as invalid. The message usually blames the token rather than the clock. See the error message reference.
The service calls are a separate model
Verifying a launch does not authorise a tool to call anything. The LTI® Advantage services use OAuth 2.0 client credentials: the tool signs a JWT assertion with its own private key, exchanges it at the platform's token endpoint for an access token scoped to specific permissions, and calls the service with that.
So a complete implementation holds a key pair of its own, publishes a key set, and manages token lifetimes — a second set of key handling on top of the first.
Scopes are granted per registration and per course. A tool should treat the absence of a service as normal rather than an error, because a platform can grant grade access in one course and withhold it in another.
Personal data
The specification does not require a platform to send a name or an email address,
and many are configured not to. Treat identity as the issuer plus the sub claim,
and everything else as optional.
Two consequences worth designing for. An integration that needs an email address to create an account will fail at institutions that send none. And matching users by email address — tempting, because it appears to solve account linking — is unsafe: addresses are reused, changed, and shared, and the same address at two institutions is not necessarily the same person.
The verification checklist
If you implement LTI® yourself, every one of these has to happen before the token is trusted:
- Signature verifies against the platform's key set, using the
kidfrom the header - Key set is fetched over a validated connection, cached, and re-fetched on an unknown
kid -
issandaudtogether match a known registration -
exphas not passed, with only a small skew allowance -
noncehas not been seen before, and is stored long enough to matter -
statematches the value this tool issued - Message type and version claims are ones you support
-
deployment_idis one you recognise for that registration
Missing any single one leaves a working integration with a hole in it, which is the difficulty: nothing fails visibly.
If you use LTIAAS, these are performed before your application is involved — your endpoint receives a launch that has already been verified, and the ltik it arrives with is what proves that to your back end. That does not remove your own obligations: session handling, account linking and authorisation inside your product remain yours.
Common questions
Is LTI® 1.3 secure?
The specification is sound — signed tokens, published keys, replay protection. Whether a given integration is secure depends on whether it performs every verification step, and skipping one is easy to do without anything appearing broken.
What stops someone forging an LTI® launch?
The signature. The ID Token is signed with the platform's private key and verified against its published key set, so a forged token fails verification. That protection only exists if the tool actually checks the signature and the issuer.
Do I need to store nonces?
Yes, if you implement LTI® yourself. The nonce is what stops a captured launch being replayed. It needs to be stored long enough to cover the token's lifetime and rejected if seen twice.
Does LTI® send personal data?
It can, and the platform decides how much. Many are configured to send no name or email, so an integration that requires them will fail at some institutions.
Next
- What is LTI® 1.3? — the launch sequence
- LTI® 1.3 vs LTI® 1.1 — why the trust model changed
- Authenticating API requests
