Level 2: 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.

Launch Profile: A Direct POST, Not the OIDC Handshake

Read this section before reaching for an off-the-shelf LTI library.

TimeBack does not perform OIDC third-party-initiated login. There is no login-initiation redirect, no state parameter, and the nonce is supplied by the launch caller rather than minted and stored by TimeBack. The platform signs an id_token carrying LTI 1.3 claims (message type LtiResourceLinkRequest, version 1.3.0), and the caller form-POSTs that token to your launch URL in a single step.

Standard LTI libraries (ltijs, the 1EdTech reference implementations) reject this out of the box, because they expect to drive the handshake and to own the nonce store. To accept a TimeBack launch, validate the token yourself:

  • Verify the signature against the JWKS for your environment, and verify iss, aud, and exp.
  • Disable the state check. No state is sent.
  • Disable the login_hint check. There is no login-initiation step to carry one.
  • Disable nonce-store replay checks. The nonce claim echoes the value the caller passed in, so TimeBack holds no record to compare it against.

This is a TimeBack-specific launch profile that reuses the LTI 1.3 token shape, not a certified LTI 1.3 implementation. A conformance-certified tool is not compatible by virtue of its certification.

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

Issuer and JWKS by environment:

Environment Issuer (iss) JWKS
Production https://timeback.com https://platform.timeback.com/.well-known/jwks.json
Sandbox https://staging.timeback.com https://sandbox.platform.timeback.com/.well-known/jwks.json

The sandbox issuer is https://staging.timeback.com, which matches neither the sandbox hostname nor the sandbox environment name: every non-production deployment stamps that one string. Two consequences when you validate a sandbox launch:

  • Pin iss to https://staging.timeback.com. Validating against the sandbox hostname or base URL fails.
  • The issuer does not tell you which environment signed the token, so it is not a substitute for pointing at the right JWKS. Each deployment publishes its own key set at its own host, so a sandbox token verifies only against the sandbox JWKS.

Sandbox is also not TimeBack's staging environment, which serves internal pre-release traffic. The two share this issuer value and nothing else.

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/authentication_method'?: 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 2 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 3 (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: Look Up User in TimeBack (Optional)

If you need to verify the user exists in TimeBack's roster or retrieve their full profile (including sourcedId for Caliper events), use the OneRoster API with email filtering:

async function lookupTimeBackUser(email: string, accessToken: string) {
  const filter = encodeURIComponent(`email='${email}'`);

  const response = await fetch(
    `https://platform.timeback.com/rostering/1.0/users?filter=${filter}&fields=sourcedId,email,givenName,familyName`,
    {
      headers: { Authorization: `Bearer ${accessToken}` },
    },
  );

  const { users } = await response.json();
  return users[0] ?? null; // Returns null if user not found
}

Example response:

{
  "users": [
    {
      "sourcedId": "550e8400-e29b-41d4-a716-446655440000",
      "email": "john.doe@example.com",
      "givenName": "John",
      "familyName": "Doe"
    }
  ]
}

This is useful when:

  • You want to verify the student is enrolled before allowing app access
  • You need the sourcedId for sending Caliper events (see Level 3)
  • You want to fetch additional roster data not included in the LTI token

For more filtering options, see OneRoster API Conventions.

Step 5: Provision User Account

Check if a user account exists in your database. 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 6: 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.

Guardians who receive an emailed login link (see Parent Login Links) arrive through the same LTI launch endpoint, but the ID token is parent-scoped and carries an extra claim:

'https://timeback.com/lti/claim/authentication_method': 'parent_email_link';

When this claim is present:

  • The LIS roles claim is always http://purl.imsglobal.org/vocab/lis/v2/membership#Member, not Learner or instructor roles.
  • The TimeBack roles claim lists only guardian-type roles (guardian, parent, relative).
  • agent_for_users lists only students with an open guardian relationship.

Your launch handler should branch on authentication_method before choosing a landing experience. A parent-link session is not a student session, even when the same person is also a student in TimeBack. Opting into parent login links does not make your tool parent-ready by itself; you still implement the parent experience.

Updating Your Registration

Once your LTI endpoint is implemented, add the LTI fields to your App. Two paths:

  • REST: PUT /applications/1.0/{sourcedApplicationId} against platform.timeback.com, where sourcedApplicationId is the applicationId you received at registration.
  • MCP tool: manage_app (action: upsert) on the platform MCP at platform.timeback.com/mcp. Pass the App's sourcedId plus the LTI fields.

Either path accepts the same fields:

Field Description Example
launchUrl (LTI Launch URL) POST endpoint that receives LTI authentication requests https://api.yourapp.com/lti/1.3/launch
landingUrl (Landing URL) Where students are redirected after authentication https://yourapp.com/
isLtiCompliant true once your endpoint is LTI 1.3 compliant true
isLtiV1P3Compliant true once your endpoint is LTI 1.3 compliant true
clientId (LTI Audience) Custom value used as the aud claim in the LTI ID token; optional — defaults to a value derived from your App your-app-audience

The aud claim TimeBack uses on the LTI token defaults to a value derived from your App; set a custom audience via the clientId field above only if your endpoint validates against a specific string.

Important: The LTI Launch URL must be a publicly accessible URL. Localhost URLs (http://localhost:...) will not work because LTI launch is server-to-server communication. TimeBack's servers need to POST directly to your endpoint over the internet.

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);

Level 0: Registration

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

Level 1: Student Onboarding

Programmatically create and manage students in TimeBack's roster before they launch your app.

Sandbox Environment

Validate a full LTI launch end to end with your sandbox credentials, without involving the TimeBack team.

Email-based guardian authentication for applications that opt in, including the parent-scoped token shape and authentication_method claim.

Level 3: Caliper Events

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

OneRoster API Conventions

Learn filtering, pagination, and field selection syntax for querying users and other roster data.