Level 1: LTI Launch
Implement seamless student authentication using LTI 1.3 so students can launch your app without login screens or credentials.
What LTI Provides
LTI 1.3 (Learning Tools Interoperability) is a 1EdTech standard that enables TimeBack to authenticate students into your app automatically. When a student clicks your app in TimeBack's catalog, they're signed in and redirected to your app's landing page—no username, no password, no friction.
How LTI Launch Works
The launch flow:
- Student clicks your app in TimeBack App
- TimeBack generates a signed OIDC ID token containing verified student identity
- TimeBack POSTs the token to your LTI launch endpoint (form submission from frontend)
- Your endpoint verifies the token signature using TimeBack's public keys
- Your endpoint provisions a user account if this is the student's first launch
- Your endpoint authenticates the student (set cookies, session, JWT, etc.)
- Your endpoint redirects (302) to your app's landing page
- Student lands in your app, fully authenticated
The ID token is a signed JWT. By verifying its signature against TimeBack's public keys (JWKS), you confirm the request is authentic and the student identity is verified.
Implementation Guide
Step 1: Create the LTI Launch Endpoint
Your endpoint receives a form-urlencoded POST request with an id_token field.
Endpoint requirements:
- Accepts
POSTrequests - Accepts
application/x-www-form-urlencodedcontent type - Extracts
id_tokenfrom form body - Returns
302 Redirectresponse
Example endpoint (Express):
app.post('/lti/1.3/launch', async (req, res) => {
const { id_token } = req.body;
// Verify token, provision user, authenticate
const redirectUrl = await handleLtiLaunch(id_token);
res.redirect(302, redirectUrl);
});
Step 2: Verify the ID Token
Use TimeBack's public keys to verify the token signature:
import { createRemoteJWKSet, jwtVerify } from 'jose';
async function verifyLtiToken(idToken: string) {
const JWKS = createRemoteJWKSet(new URL('https://platform.timeback.com/.well-known/jwks.json'));
const verified = await jwtVerify(idToken, JWKS, {
issuer: 'https://timeback.com',
audience: 'your-app-audience', // Provided during registration
});
return verified.payload;
}
Token validation:
- Verify signature using JWKS
- Verify
iss(issuer) matches TimeBack - Verify
aud(audience) matches your app's audience identifier - Verify
exp(expiration) is in the future
Step 3: Extract User Identity
The verified token payload contains student information:
interface LtiTokenPayload {
// Standard OIDC claims
sub: string; // Platform user ID
email: string;
name: string;
given_name: string;
family_name: string;
// LTI 1.3 claims
'https://purl.imsglobal.org/spec/lti/claim/message_type': 'LtiResourceLinkRequest';
'https://purl.imsglobal.org/spec/lti/claim/version': '1.3.0';
'https://purl.imsglobal.org/spec/lti/claim/target_link_uri': string;
// TimeBack-specific claims
'https://timeback.com/lti/claim/application_id': string;
'https://timeback.com/lti/claim/tool_id': string;
}
function extractUserIdentity(payload: LtiTokenPayload) {
return {
platformId: payload.sub,
email: payload.email,
fullName: payload.name || `${payload.given_name} ${payload.family_name}`,
};
}
Critical: Store the sub field
Even if you're only implementing Level 1 now, you should store the sub value in your database alongside the user record. This is the student's TimeBack platform ID.
Why this matters:
- If you later implement Level 2 (Caliper events), you'll use
subas theactorfield in all events - This links student activity in your app to their TimeBack profile
- Without it, you cannot send Caliper events—there's no other way to identify which TimeBack user your events refer to
- The
subvalue remains constant across all sessions and never changes
Step 4: Provision User Account
Check if a user account exists for this email. If not, create one:
async function ensureUser(email: string, fullName: string, platformId: string) {
let user = await database.findUserByEmail(email);
if (!user) {
user = await database.createUser({
email,
fullName,
platformId,
});
}
return user;
}
Step 5: Authenticate and Redirect
Set your app's authentication mechanism (cookies, session, JWT, etc.) and redirect to the landing page:
async function handleLtiLaunch(idToken: string): Promise<string> {
// 1. Verify token
const payload = await verifyLtiToken(idToken);
// 2. Extract identity
const { email, fullName, platformId } = extractUserIdentity(payload);
// 3. Provision user
const user = await ensureUser(email, fullName, platformId);
// 4. Authenticate (example: set session cookie)
const sessionToken = await createSessionToken(user.id);
setAuthCookie(sessionToken);
// 5. Redirect to landing page
const targetUrl = payload['https://purl.imsglobal.org/spec/lti/claim/target_link_uri'];
return targetUrl || 'https://yourapp.com/';
}
Authentication options:
- Set session cookies (most common for web apps)
- Generate your own JWT and set as cookie
- Create magic link token and redirect with query param
- Use your existing authentication mechanism
The key requirement: students must be authenticated when they land on your app.
Updating Your Registration
Once your LTI endpoint is implemented, reply to your original registration email with these fields:
| Field | Description | Example |
|---|---|---|
| LTI Launch URL | POST endpoint that receives LTI authentication requests | https://api.yourapp.com/lti/1.3/launch |
| LTI Audience | Unique identifier for your app (used in token validation) | yourapp |
| Landing URL | Where students are redirected after authentication | https://yourapp.com/ |
LTI Audience: This is an arbitrary string you choose. We include it as the aud claim in the ID token. Your endpoint verifies that aud matches this value, ensuring the token is intended for your app.
Common Pitfalls
Not verifying token signature
// ❌ Bad: Decode without verification
const payload = JSON.parse(Buffer.from(idToken.split('.')[1], 'base64').toString());
// Anyone can forge this token
// ✅ Good: Verify signature with JWKS
const { payload } = await jwtVerify(idToken, JWKS, { issuer, audience });
Wrong content type for endpoint
// ❌ Bad: Expecting JSON
app.post('/lti/launch', express.json(), handler);
// ✅ Good: Form-urlencoded
app.post('/lti/launch', express.urlencoded({ extended: true }), handler);
Related Docs
Level 0: Registration
Register your app first before implementing LTI. You'll need your app registered to receive the audience identifier.
Authentication
LTI handles student authentication, but you'll still need OAuth client credentials to send Caliper events (Level 2).
Level 2: Caliper Events
After implementing LTI, send learning activity events to unlock coaching insights and unified analytics.