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:

  1. Student clicks your app in TimeBack App
  2. TimeBack generates a signed OIDC ID token containing verified student identity
  3. TimeBack POSTs the token to your LTI launch endpoint (form submission from frontend)
  4. Your endpoint verifies the token signature using TimeBack's public keys
  5. Your endpoint provisions a user account if this is the student's first launch
  6. Your endpoint authenticates the student (set cookies, session, JWT, etc.)
  7. Your endpoint redirects (302) to your app's landing page
  8. 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 POST requests
  • Accepts application/x-www-form-urlencoded content type
  • Extracts id_token from form body
  • Returns 302 Redirect response

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 sub as the actor field 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 sub value 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);

Register your app first before implementing LTI. You'll need your app registered to receive the audience identifier.

LTI handles student authentication, but you'll still need OAuth client credentials to send Caliper events (Level 2).

After implementing LTI, send learning activity events to unlock coaching insights and unified analytics.