Skip to main content
Version: 2026-08-25 (archived)

Verifying the launch JWT

The launch payload is a single launch_data field: a signed JWT (JWS) that carries the whole launch. It is signed with Voshi's system key, so you verify it against Voshi's public keys, published at the JWKS endpoint:

https://api.link.voshi.com/lti13/v1/jwks

(Also available at https://api.link.voshi.com/.well-known/jwks.)

Your API key is not involved in launch verification — it is only for the grades and management APIs.

What to check​

CheckDetails
AlgorithmRS256 (RSA, 2048-bit). Pass an explicit algorithm allowlist to your JWT library.
SignatureVerify against the JWKS key whose kid matches the token's kid header (always present).
Expiryexp and iat are in seconds. Treat an expired token as an invalid launch. Tokens are valid for 2 hours after iat.
ReplayThe lifetime is bounded but not tight, so don't accept the same token twice — trade it for your own session on first use.
warning

Keys rotate (roughly monthly), and during rotation the JWKS contains more than one key. Fetch the JWKS dynamically and cache by kid — don't pin a single key. The JWKS clients below do this for you.

Code​

# pip install pyjwt cryptography
import jwt
from jwt import PyJWKClient

JWKS_URL = "https://api.link.voshi.com/lti13/v1/jwks"
jwks = PyJWKClient(JWKS_URL) # create once — it fetches and caches keys by kid

def read_launch_data(token: str) -> dict:
signing_key = jwks.get_signing_key_from_jwt(token)
# decode() verifies the signature and raises on an expired `exp`.
return jwt.decode(token, signing_key.key, algorithms=["RS256"])

Both clients handle key rotation transparently: when a token arrives with a kid they haven't cached, they re-fetch the JWKS.

Common pitfalls​

  • Decoding without verifying. Most JWT libraries have a "decode only" mode (jwt.decode(..., options={"verify_signature": False}), jwt.decode vs jwtVerify). Never use it on launch_data — an attacker can mint arbitrary claims in an unverified token.
  • Skipping the algorithm allowlist. Always pass algorithms=["RS256"] explicitly so a manipulated header can't downgrade verification.
  • Pinning one key. A hardcoded public key works until the next rotation, then every launch fails. Use a JWKS client.
  • Treating the token as a session. The token is valid for 2 hours and could be replayed within that window by anyone who obtains it. Exchange it for your own session cookie immediately, and reject reuse.

Once the token verifies, every claim inside it is trustworthy. On to the Launch Data reference.