Skip to Content
MoodlePartie 10 — LTI (Learning Tools Interoperability)Moodle en tant que plateforme : consommer des outils LTI

Chapitre 10.3 — Moodle en tant que plateforme : consommer des outils LTI

Dans les deux chapitres précédents, nous avons disséqué le protocole LTI 1.3 : OpenID Connect en mode third-party initiated login, JWT signés, services LTI Advantage (AGS, NRPS, Deep Linking). Tout cela restait volontairement abstrait — un « outil » d’un côté, une « plateforme » de l’autre. Il est temps de rendre les choses concrètes : dans ce chapitre, Moodle joue le rôle de plateforme (platform, anciennement tool consumer), et notre application Next.js fil rouge QuizLab (https://tool.example.com) joue le rôle d’outil.

Concrètement, cela signifie que Moodle :

  • émet les JWT de lancement (id_token) signés avec sa clé privée ;
  • expose un endpoint d’autorisation OIDC, un endpoint de token OAuth 2.0 et un JWKS public ;
  • reçoit les notes que l’outil lui renvoie via AGS et les écrit dans son carnet de notes ;
  • fournit la liste des inscrits via NRPS si on l’y autorise ;
  • orchestre le Deep Linking pour que l’enseignant choisisse du contenu directement dans l’outil.

Toute cette machinerie vit dans un seul module d’activité : mod_lti, alias l’activité « External tool » (Outil externe). Ce chapitre est le guide de terrain : chaque écran d’administration, chaque champ, chaque case à cocher, et — parce que c’est là que vous passerez le plus de temps — une section complète de débogage.

Ce chapitre suppose Moodle 5.2 (avril 2026). Les chemins d’administration et libellés cités sont ceux de l’interface anglaise, avec traduction française entre parenthèses.

Sommaire

  1. Vue d’ensemble : où LTI apparaît dans Moodle
  2. Ajouter un outil LTI 1.3 : la configuration manuelle
  3. Ajouter un outil LTI 1.3 : le Dynamic Registration
  4. Portée des outils : site, cours, instance
  5. L’activité « External tool » dans un cours
  6. Paramètres custom et variables de substitution
  7. Réception des notes : AGS côté plateforme
  8. Confidentialité et RGPD
  9. Débogage côté Moodle
  10. Ce qu’il faut retenir

1. Vue d’ensemble : où LTI apparaît dans Moodle

1.1 mod_lti, l’implémentation « platform » de Moodle

Historiquement, le module mod_lti s’appelait « External tool » et ne faisait qu’une chose : lancer un outil LTI 1.0/1.1 avec une signature OAuth 1.0a. Depuis Moodle 3.7, il implémente LTI 1.3 et l’ensemble de LTI Advantage. En Moodle 5.2, c’est une implémentation platform certifiée par 1EdTech qui couvre :

SpécificationSupport côté Moodle plateforme
LTI 1.3 Core (launch OIDC + JWT)Complet
Deep Linking 2.0Complet (sélection de contenu, création multiple)
Assignment and Grade Services (AGS) 2.0Complet (line items, scores, résultats)
Names and Role Provisioning Services (NRPS) 2.0Complet
Dynamic RegistrationComplet (Moodle 3.10+)
LTI 1.1 / 1.0Toujours supporté (legacy, à éviter pour du neuf)
Basic Outcomes 1.1Supporté pour les outils 1.1 legacy

Le code vit dans mod/lti/ de l’arborescence Moodle. Les endpoints que Moodle expose en tant que plateforme — vous les donnerez à chaque outil que vous enregistrez — sont, pour notre plateforme d’exemple https://moodle.example.com :

RôleURL
Platform ID (issuer iss)https://moodle.example.com
Authentication request URL (autorisation OIDC)https://moodle.example.com/mod/lti/auth.php
Access token URL (endpoint de token OAuth 2.0)https://moodle.example.com/mod/lti/token.php
Public keyset URL (JWKS)https://moodle.example.com/mod/lti/certs.php
OpenID configuration (pour le Dynamic Registration)https://moodle.example.com/mod/lti/openid-configuration.php

💡 Pour un dev React : si vous développez QuizLab, ces trois URL (auth.php, token.php, certs.php) sont exactement ce que votre bibliothèque LTI (ltijs, @atomicjolt/lti-server, ou votre implémentation maison avec jose) vous demandera lors de l’enregistrement de la plateforme. L’iss de Moodle est toujours l’URL racine du site, sans slash final — c’est la valeur de $CFG->wwwroot. Un mismatch d’un simple / final dans votre config d’issuer fait échouer la validation du JWT.

1.2 Où LTI apparaît dans l’interface

En tant qu’utilisateur, on rencontre LTI à quatre endroits :

  1. L’activité « External tool » (Outil externe) dans le sélecteur d’activités d’un cours. C’est le mode générique : l’enseignant ajoute une activité, choisit un outil préconfiguré (ou saisit une URL), et l’activité apparaît dans le cours comme n’importe quel devoir ou quiz.

  2. Le sélecteur d’activités lui-même : un outil préconfiguré avec l’option « Show in activity chooser » apparaît comme un type d’activité à part entière, avec son propre nom et sa propre icône. L’enseignant voit « QuizLab » à côté de « Quiz » et « Assignment », sans savoir que c’est du LTI sous le capot. C’est l’expérience recommandée depuis Moodle 4.x.

  3. La page « LTI External tools » au niveau du cours (Course navigation > More > LTI External tools) : depuis Moodle 4.3, les enseignants (avec la capability adéquate) peuvent y voir les outils disponibles et en configurer de nouveaux au niveau du cours. Nous y revenons en section 4.

  4. Les administrateurs passent par Site administration > Plugins > Activity modules > External tool > Manage tools (Administration du site > Plugins > Modules d’activité > Outil externe > Gérer les outils) — le centre névralgique de ce chapitre.

À noter : Moodle 5.x a aussi introduit le placement des outils LTI comme boutons de contenu dans certains contextes d’édition (sélection de contenu via Deep Linking depuis l’éditeur), mais le cœur du sujet reste l’activité External tool.

📚 Aller plus loin : la page de référence côté utilisateur est docs.moodle.org/502/en/LTI_External_tools  et, côté développeur, moodledev.io — LTI . La certification 1EdTech de Moodle est consultable sur le site d’1EdTech (catalogue « TrustEd Apps »).


2. Ajouter un outil LTI 1.3 : la configuration manuelle

2.1 Le point d’entrée : Manage tools

Rendez-vous dans :

Site administration > Plugins > Activity modules > External tool > Manage tools (Administration du site > Plugins > Modules d'activité > Outil externe > Gérer les outils)

La page présente en haut un champ « Tool URL… » accompagné du bouton « Add LTI Advantage » (c’est la voie du Dynamic Registration, section 3), et juste en dessous un lien « configure a tool manually » (configurer un outil manuellement). Plus bas, la liste des outils existants sous forme de cartes, chacune avec :

  • un menu contextuel (« ⋯ ») : Edit, Delete, et surtout « View configuration details » (Afficher les détails de configuration) ;
  • un compteur d’utilisation (« Used X times ») ;
  • un état : Active, ou Pending pour un outil issu d’un Dynamic Registration pas encore activé.

Cliquez sur « configure a tool manually ». Passons en revue tous les champs du formulaire, dans l’ordre.

2.2 Tool settings (Réglages de l’outil)

Tool name (Nom de l’outil)

Le nom affiché aux enseignants dans le sélecteur d’outil préconfiguré et, si l’outil est exposé dans le sélecteur d’activités, le nom du « type d’activité ». Pour notre fil rouge : QuizLab.

Tool URL (URL de l’outil)

L’URL de lancement cible (le target_link_uri par défaut). C’est là que Moodle enverra l’utilisateur à la fin du flow OIDC si l’instance ne définit rien d’autre. Pour QuizLab : https://tool.example.com/api/lti/launch.

Attention à la double casquette de ce champ : il sert aussi de clé de correspondance quand un enseignant saisit une URL directement dans une activité External tool sans choisir d’outil préconfiguré — Moodle compare l’URL saisie au Tool URL (et au domaine) des outils enregistrés pour retrouver la bonne configuration de sécurité.

Tool description (Description de l’outil)

Texte affiché aux enseignants dans le sélecteur d’activités. Purement informatif.

LTI version

Sélectionnez « LTI 1.3 ». Les autres valeurs (LTI 1.0/1.1, LTI 2.0 déprécié) déclenchent l’ancien monde OAuth 1.0a avec consumer key / shared secret — hors périmètre pour du développement neuf.

Dès que « LTI 1.3 » est sélectionné, le formulaire révèle les champs cryptographiques suivants.

Public key type (Type de clé publique)

Deux options :

  • « Keyset URL » (recommandé) : Moodle ira chercher les clés publiques de l’outil sur son endpoint JWKS à chaque fois qu’il doit vérifier une signature (réponse Deep Linking, assertion client JWT pour obtenir un token de service). Pour QuizLab : https://tool.example.com/api/lti/jwks. C’est le seul mode qui supporte la rotation de clés côté outil.
  • « RSA key » : vous collez directement la clé publique PEM de l’outil dans un textarea. À réserver aux outils qui n’exposent pas de JWKS. Si l’outil change de clé, il faut revenir éditer la configuration à la main.

⚠️ Piège : en mode Keyset URL, Moodle effectue une requête HTTP sortante vers l’outil au moment de vérifier une signature. Si votre outil de dev tourne sur https://localhost:3000 avec un certificat auto-signé, ou derrière un firewall que le serveur Moodle ne peut pas traverser, la vérification échouera avec des erreurs peu parlantes (« Consumer key is incorrect » sur d’anciennes versions, erreurs de token sur les récentes). En développement, soit exposez QuizLab via un tunnel (ngrok, Cloudflare Tunnel), soit collez la clé publique en mode « RSA key ».

Initiate login URL (URL d’initiation de connexion)

L’endpoint OIDC login initiation de l’outil, première étape du lancement (chapitre 10.1). Pour QuizLab : https://tool.example.com/api/lti/login. Moodle y enverra iss, login_hint, lti_message_hint, target_link_uri, client_id et lti_deployment_id.

Redirection URI(s)

La liste (une par ligne) des URI de redirection autorisées — les URL vers lesquelles l’outil a le droit de demander à Moodle de renvoyer l’id_token. Le paramètre redirect_uri que QuizLab enverra dans sa requête d’autorisation doit figurer exactement dans cette liste, sinon Moodle refuse le lancement. Typiquement :

https://tool.example.com/api/lti/launch

Ajoutez-y toute URL de deep link ou de launch alternatif que l’outil utilise.

⚠️ Piège : la comparaison est stricte (schéma, hôte, port, chemin). https://tool.example.com/api/lti/launch/ (slash final) ≠ https://tool.example.com/api/lti/launch. Le symptôme est une erreur au retour de auth.php : « Unknown or unspecified redirect_uri ». C’est l’une des trois erreurs les plus fréquentes lors d’un premier branchement.

Custom parameters (Paramètres personnalisés)

Paires nom=valeur, une par ligne, injectées dans le claim https://purl.imsglobal.org/spec/lti/claim/custom de tous les lancements de cet outil (niveau outil ; il existe aussi un champ au niveau de chaque instance d’activité, voir sections 5 et 6). Exemple :

environment=production quizlab_org=universite-exemple courseid=$Context.id

La syntaxe complète et les variables $... sont détaillées en section 6.

Tool configuration usage (Utilisation de la configuration de l’outil)

Contrôle la visibilité de l’outil pour les enseignants. Trois options :

  1. « Show in activity chooser and as a preconfigured tool » (Afficher dans le sélecteur d’activités et comme outil préconfiguré) : l’outil apparaît comme un type d’activité à part entière et dans la liste déroulante des outils préconfigurés d’une activité External tool. Le choix recommandé pour un outil destiné aux enseignants.
  2. « Show as preconfigured tool when adding an external tool » (Afficher comme outil préconfiguré lors de l’ajout d’un outil externe) : visible uniquement dans la liste déroulante de l’activité External tool, pas dans le sélecteur d’activités. Moins de visibilité, moins de bruit.
  3. « Do not show; use only when a matching tool URL is entered » (Ne pas afficher ; utiliser seulement quand une URL correspondante est saisie) : l’outil est invisible, mais si un enseignant saisit une URL qui matche le Tool URL/domaine, Moodle applique silencieusement cette configuration (clés, services, confidentialité). Utile pour sécuriser des URL sans exposer l’outil.

Default launch container (Conteneur de lancement par défaut)

Comment l’outil s’affiche quand un utilisateur lance l’activité :

  • « Default » : hérite du réglage global du module.
  • « Embed » (Intégré) : iframe dans la page Moodle, avec les blocs et la navigation Moodle autour.
  • « Embed, without blocks » (Intégré, sans blocs) : iframe pleine largeur, sans les blocs latéraux. Le plus utilisé pour une intégration « seamless ».
  • « New window » (Nouvelle fenêtre) : ouverture dans un nouvel onglet/fenêtre ; la page Moodle affiche un lien de secours.
  • « Existing window » (Fenêtre existante) : navigation complète de la fenêtre courante vers l’outil (l’utilisateur quitte Moodle).

💡 Pour un dev React : le choix Embed vs New window n’est pas cosmétique, c’est un choix d’architecture de session. En iframe, votre app Next.js vit dans un contexte tiers : ses cookies doivent être SameSite=None; Secure, et Safari (ITP) peut les bloquer complètement. Si QuizLab stocke sa session dans un cookie classique, elle fonctionnera en « New window » et sera vide en « Embed ». Les stratégies de contournement (state dans l’URL, postMessage, CHIPS Partitioned) sont détaillées en section 9.4. En attendant, pendant le développement, configurez « New window » : vous éliminez toute une classe de bugs.

Supports Deep Linking (Content-Item Message)

Case à cocher. Si activée, l’activité External tool affichera un bouton « Select content » (Sélectionner un contenu) qui lance l’outil en mode LtiDeepLinkingRequest au lieu du launch classique. L’expérience enseignant est décrite en section 5.3.

Content Selection URL (URL de sélection de contenu)

L’URL de lancement à utiliser pour les requêtes Deep Linking, si elle diffère du Tool URL. Beaucoup d’outils utilisent le même endpoint et routent selon le message_type du JWT ; QuizLab fait pareil, donc on peut laisser vide (Moodle utilisera Tool URL). Si votre outil a un endpoint dédié, par exemple https://tool.example.com/api/lti/deep-link, c’est ici qu’il se déclare — et n’oubliez pas de l’ajouter aux Redirection URIs.

Icon URL / Secure icon URL

URL d’une icône affichée dans le sélecteur d’activités et à côté des instances. Optionnel mais fortement recommandé pour l’expérience enseignant.

2.3 Services (Services)

Section cruciale : c’est ici que l’admin décide quels services LTI Advantage l’outil pourra invoquer, c’est-à-dire quels scopes OAuth Moodle acceptera de lui délivrer sur token.php.

IMS LTI Assignment and Grade Services (Services de notes et devoirs)

Trois options :

  1. « Do not use this service » (Ne pas utiliser ce service) : aucun scope AGS n’est accordé ; le claim endpoint est absent du JWT de lancement. L’outil ne peut pas renvoyer de notes par AGS.
  2. « Use this service for grade sync and column management » (Utiliser ce service pour la synchronisation des notes et la gestion des colonnes) — la valeur par défaut et le choix standard. Moodle accorde les scopes :
    • https://purl.imsglobal.org/spec/lti-ags/scope/lineitem (créer/modifier/supprimer des line items),
    • https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly,
    • https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly (lire les résultats),
    • https://purl.imsglobal.org/spec/lti-ags/scope/score (poster des scores).
  3. « Use this service for grade sync only » (Utiliser ce service pour la synchronisation des notes seulement) : l’outil peut poster des scores et lire les line items existants, mais pas en créer ou en gérer (lineitem.readonly + score + result.readonly, sans lineitem en écriture). L’outil est confiné à la colonne créée par Moodle pour l’instance.

Le fonctionnement effectif d’AGS côté Moodle (création de colonnes, conversion des scores) est décrit en section 7.

IMS LTI Names and Role Provisioning (Approvisionnement des noms et rôles)

Deux options :

  • « Use this service to retrieve members’ information as per privacy settings » (Utiliser ce service pour récupérer les informations des membres selon les réglages de confidentialité) — défaut : l’outil obtient le scope https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly et le claim names_role_service dans le launch. Il peut alors lister les inscrits du cours ; les champs nom/email de chaque membre sont filtrés selon les réglages Privacy (section 2.4) — d’où le libellé.
  • « Do not use this service ».

Tool Settings (Réglages de l’outil / service LTI Tool Settings)

  • « Use this service » / « Do not use this service ». Il s’agit du service (peu connu) LTI Tool Settings qui permet à l’outil de stocker des paires clé/valeur côté plateforme, à trois niveaux (déploiement, contexte, resource link). Moodle les persiste dans la table mdl_lti_tool_settings. Peu d’outils l’utilisent ; laissez la valeur par défaut sauf besoin explicite.

2.4 Privacy (Confidentialité)

Quatre réglages qui gouvernent ce que Moodle enverra à l’outil :

  • Share launcher’s name with tool (Partager le nom du lanceur avec l’outil) : Never / Delegate to teacher / Always. Si « Always », le JWT de lancement contient name, given_name, family_name. Si « Delegate to teacher », une case apparaît dans les réglages de chaque instance d’activité et c’est l’enseignant qui décide.
  • Share launcher’s email with tool (Partager l’adresse de courriel du lanceur avec l’outil) : mêmes trois options ; contrôle le claim email.
  • Accept grades from the tool (Accepter des notes de l’outil) : Never / Delegate to teacher / Always. Contrôle si le lancement expose les capacités de retour de note et si l’instance crée une colonne dans le carnet de notes. Interaction avec AGS détaillée en section 7.5.
  • Force SSL (Forcer SSL) : exige HTTPS pour toutes les communications avec l’outil. En 2026, laissez coché ; les navigateurs bloquent de toute façon le contenu mixte en iframe.

Vient enfin la section Miscellaneous (Divers) avec « Organization ID » (identifiant envoyé dans le claim tool_consumer_instance_guid / claim platform), « Organization URL », et le géo-référencement optionnel. Valeurs par défaut acceptables dans 99 % des cas.

Cliquez sur « Save changes ».

2.5 Ce que Moodle génère après l’enregistrement

C’est le moment clé, souvent raté par les débutants : l’enregistrement est bidirectionnel. Vous venez de donner à Moodle les URL de QuizLab ; il faut maintenant donner à QuizLab les identifiants que Moodle vient de générer.

Sur la carte de l’outil dans Manage tools, ouvrez le menu et cliquez sur « View configuration details » (Afficher les détails de configuration). Une modale affiche :

Champ affichéExempleCorrespondance protocole
Platform IDhttps://moodle.example.comiss (issuer) des JWT
Client IDa1b2c3d4e5f6g7haud des JWT, client_id OAuth
Deployment ID1claim https://purl.imsglobal.org/spec/lti/claim/deployment_id
Public keyset URLhttps://moodle.example.com/mod/lti/certs.phpJWKS de la plateforme
Access token URLhttps://moodle.example.com/mod/lti/token.phpendpoint token OAuth 2.0 (client_credentials + JWT assertion)
Authentication request URLhttps://moodle.example.com/mod/lti/auth.phpendpoint d’autorisation OIDC

Le client_id est généré aléatoirement par Moodle (chaîne alphanumérique) ; le deployment_id est simplement l’identifiant interne du type d’outil (l’id de la ligne dans mdl_lti_types) — d’où des valeurs comme 1, 2, 3.

Copiez ces six valeurs dans la configuration de QuizLab. Avec ltijs par exemple :

// tool.example.com — enregistrement de la plateforme Moodle côté QuizLab await lti.registerPlatform({ url: 'https://moodle.example.com', // Platform ID (iss) name: 'Moodle Université Exemple', clientId: 'a1b2c3d4e5f6g7h', // Client ID authenticationEndpoint: 'https://moodle.example.com/mod/lti/auth.php', accesstokenEndpoint: 'https://moodle.example.com/mod/lti/token.php', authConfig: { method: 'JWK_SET', key: 'https://moodle.example.com/mod/lti/certs.php', // Public keyset URL }, });

⚠️ Piège : un même site Moodle a un seul issuer (https://moodle.example.com) mais un client_id différent par outil enregistré, et potentiellement plusieurs deployment_id par client_id (rare avec Moodle, qui crée en pratique un déploiement par type d’outil). Côté QuizLab, la clé de lookup d’une plateforme doit être le couple (iss, client_id), pas iss seul — sinon deux enregistrements du même outil sur le même Moodle (par exemple un « test » et un « prod ») se marcheront dessus.

📚 Aller plus loin : la sémantique exacte issuer / client_id / deployment_id est définie dans le LTI 1.3 Core, section « Platforms and deployments » . Le modèle mental correct : issuer = le système, client_id = l’enregistrement de sécurité (clés, scopes), deployment_id = le contexte de déploiement de cet enregistrement.


3. Ajouter un outil LTI 1.3 : le Dynamic Registration

Recopier douze URL et identifiants à la main dans deux systèmes, c’est laborieux et source d’erreurs. La spécification LTI Dynamic Registration (1EdTech, 2020) automatise l’échange : l’admin colle une seule URL et tout le reste se négocie machine à machine. Moodle la supporte nativement, et c’est la voie mise en avant dans l’UI (« Add LTI Advantage »).

3.1 Le flow, étape par étape

  1. L’admin ouvre Manage tools, colle l’URL de registration de l’outil dans le champ « Tool URL… » — pour QuizLab : https://tool.example.com/api/lti/register — et clique « Add LTI Advantage ».
  2. Moodle ouvre cette URL (dans une iframe/modale) en lui ajoutant deux paramètres de requête :
    • openid_configuration : l’URL de la configuration OpenID de Moodle, par exemple https://moodle.example.com/mod/lti/openid-configuration.php. Ce document JSON décrit la plateforme : issuer, endpoints, scopes supportés, claims, et surtout le registration endpoint ;
    • registration_token : un jeton Bearer à usage limité que l’outil devra présenter pour s’enregistrer.
  3. Le endpoint /api/lti/register de QuizLab (côté serveur) fait un GET sur l’URL openid_configuration et lit la réponse.
  4. QuizLab construit sa propre description (endpoints, claims souhaités, messages supportés, scopes AGS/NRPS demandés) au format OpenID Connect Dynamic Client Registration étendu par le claim https://purl.imsglobal.org/spec/lti-tool-configuration, et la POST au registration_endpoint de Moodle avec le header Authorization: Bearer <registration_token>.
  5. Moodle valide, crée le type d’outil, génère un client_id et un deployment_id, et les renvoie dans la réponse JSON. QuizLab persiste ces valeurs immédiatement — c’est l’équivalent automatisé de la modale « View configuration details ».
  6. QuizLab signale la fin à la fenêtre parente via postMessage ({subject: 'org.imsglobal.lti.close'}) ; la modale se ferme.
  7. L’outil apparaît dans Manage tools avec le statut « Pending » (En attente). Comme le dit la doc Moodle : « The pending state allows you to review the tool configuration, the privacy and services granted » — l’admin relit la configuration (services accordés, réglages de confidentialité proposés par l’outil), l’ajuste au besoin, puis clique « Activate » (Activer). Tant que l’outil est Pending, aucun lancement n’est possible.

3.2 Diagramme de séquence

3.3 Ce que QuizLab envoie et reçoit (exemple concret)

Requête d’enregistrement type que QuizLab poste au registration_endpoint de Moodle :

{ "application_type": "web", "response_types": ["id_token"], "grant_types": ["implicit", "client_credentials"], "initiate_login_uri": "https://tool.example.com/api/lti/login", "redirect_uris": ["https://tool.example.com/api/lti/launch"], "client_name": "QuizLab", "jwks_uri": "https://tool.example.com/api/lti/jwks", "logo_uri": "https://tool.example.com/logo.png", "token_endpoint_auth_method": "private_key_jwt", "scope": "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem https://purl.imsglobal.org/spec/lti-ags/scope/score https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly", "https://purl.imsglobal.org/spec/lti-tool-configuration": { "domain": "tool.example.com", "target_link_uri": "https://tool.example.com/api/lti/launch", "custom_parameters": { "environment": "production" }, "claims": ["iss", "sub", "name", "email"], "messages": [ { "type": "LtiDeepLinkingRequest", "target_link_uri": "https://tool.example.com/api/lti/launch", "label": "Ajouter un quiz QuizLab" } ] } }

La réponse de Moodle reprend ce document, complété notamment par :

{ "client_id": "a1b2c3d4e5f6g7h", "https://purl.imsglobal.org/spec/lti-tool-configuration": { "deployment_id": "1", "...": "..." } }

⚠️ Piège : les scopes et claims que l’outil demande ne sont pas automatiquement accordés. Moodle mappe la demande sur ses réglages (AGS, NRPS, privacy) et l’admin peut tout dégrader pendant l’état Pending. Côté QuizLab, ne présumez jamais qu’un scope est disponible : lisez la réponse d’enregistrement, et à chaque lancement, lisez les claims endpoint.scope et names_role_service effectivement présents dans le JWT.

💡 Pour un dev React : dans une app Next.js, /api/lti/register est une route serveur qui rend une petite page (le temps de faire les deux appels HTTP côté serveur, via une Server Action ou un route handler) puis exécute le postMessage de fermeture dans un <script>. Point d’attention : la page s’affiche dans une iframe Moodle, donc pas de dépendance à vos cookies de session ici non plus. Faites tout le travail dans le handler (les paramètres openid_configuration et registration_token arrivent en query string), et ne rendez au client que le script de fermeture.

📚 Aller plus loin : spécification LTI Dynamic Registration 1.0  ; côté Moodle, la page moodledev.io « LTI Advantage registration » documente les particularités de l’implémentation (dont le fait que Moodle n’accepte qu’un seul message LtiDeepLinkingRequest dans messages).


4. Portée des outils : site, cours, instance

Moodle offre trois niveaux de configuration d’un outil LTI, du plus industrialisé au plus artisanal.

4.1 Outils au niveau site (préconfigurés par l’admin)

C’est tout ce qu’on vient de faire en sections 2 et 3 : un outil déclaré dans Manage tools est disponible (selon son « Tool configuration usage ») dans tous les cours du site. Avantages :

  • une seule paire de clés, un seul client_id, gérés par l’admin ;
  • les réglages de sécurité et de confidentialité sont uniformes et audités ;
  • l’outil peut apparaître dans le sélecteur d’activités avec son nom et son icône.

Depuis Moodle 4.3+, l’admin peut aussi restreindre un outil de site à certaines catégories de cours (« Restrict to category »), pratique pour un outil sous licence facultaire.

4.2 Outils au niveau cours (« course tools »)

Depuis Moodle 4.3, la page Course navigation > More > LTI External tools (Navigation du cours > Plus > Outils externes LTI) permet à un enseignant :

  • de voir les outils de site disponibles dans son cours et de basculer leur visibilité dans le sélecteur d’activités du cours (« Show in activity chooser » — interrupteur par cours) ;
  • de configurer un nouvel outil propre au cours (bouton « Add tool »), avec un sous-ensemble des champs admin, à condition de posséder la capability mod/lti:addcoursetool.

Un outil de cours n’existe que dans ce cours : client_id dédié, invisible ailleurs. C’est le bon compromis pour un enseignant qui expérimente un outil sans déranger l’admin — mais côté outil, cela signifie autant d’enregistrements que de cours, ce qui ne passe pas à l’échelle. Le Dynamic Registration au niveau cours n’est pas proposé partout ; en pratique la saisie est manuelle.

4.3 Configuration manuelle par instance (legacy)

Dernier niveau : dans une activité External tool, l’enseignant peut ignorer les outils préconfigurés et saisir directement une URL (et, en LTI 1.1, une clé/un secret). En LTI 1.3 ce mode n’a plus vraiment de sens — il n’y a pas de champ pour coller un JWKS au niveau instance — et il ne survit que par la magie du « Do not show; use only when a matching tool URL is entered » : l’URL saisie est rattachée à une configuration de site par correspondance de domaine.

4.4 Recommandations

SituationNiveau recommandé
Outil institutionnel (anti-plagiat, éditeur, QuizLab en prod)Site, via Dynamic Registration, « Show in activity chooser »
Pilote sur une facultéSite + « Restrict to category »
Expérimentation d’un enseignantCourse tool
Développement / debug de QuizLabSite sur un Moodle de dev, config manuelle (contrôle total des champs)
Saisie d’URL libre par les enseignantsÀ proscrire ; si nécessaire, mode « Do not show; use only when a matching tool URL is entered »

⚠️ Piège : un outil configuré au niveau cours puis « promu » au niveau site est un nouvel enregistrement (nouveau client_id). Les instances d’activité existantes restent liées à l’ancien type d’outil. Migrer proprement implique de mettre à jour le champ typeid des instances (table mdl_lti) — opération de base de données, pas d’interface pour ça.


5. L’activité « External tool » dans un cours

5.1 Ajouter l’activité

Deux chemins pour l’enseignant, en mode édition :

  • si l’outil est « Show in activity chooser » : Add an activity or resource > tuile QuizLab — le formulaire s’ouvre avec l’outil présélectionné ;
  • sinon : Add an activity or resource > External tool (Outil externe), puis choisir l’outil dans la liste déroulante « Preconfigured tool » (Outil préconfiguré).

5.2 Les champs de l’instance

  • Activity name (Nom de l’activité) : le nom affiché sur la page de cours. Peut être pré-rempli par le Deep Linking.
  • Preconfigured tool : le type d’outil, avec à côté le bouton « Select content » si le Deep Linking est supporté.
  • Tool URL (sous « Show more… ») : URL de lancement spécifique à cette instance, si elle diffère de celle de l’outil ; c’est elle qui deviendra le target_link_uri du launch.
  • Launch container : surcharge du conteneur par défaut de l’outil (Default / Embed / Embed without blocks / New window / Existing window).
  • Custom parameters (sous « Show more… ») : paramètres custom de l’instance, fusionnés avec ceux de l’outil (section 6).
  • Privacy (si l’admin a choisi « Delegate to teacher ») : cases « Share launcher’s name with tool », « Share launcher’s email with tool », « Accept grades from the tool ».
  • Grade (Note) : type de note (points/barème), note maximale, catégorie du carnet de notes, note pour passer. Ces réglages définissent la colonne de carnet de notes liée à l’instance — celle qu’AGS alimentera par défaut (section 7).
  • Puis les sections standard de toute activité : Common module settings, Restrict access, Activity completion, Tags, Competencies.

5.3 « Select content » : l’expérience Deep Linking côté enseignant

Quand l’outil supporte le Deep Linking, le bouton « Select content » transforme l’expérience :

  1. L’enseignant clique « Select content ». Moodle lance l’outil en LtiDeepLinkingRequest (dans une modale/iframe), avec un claim deep_linking_settings qui contient notamment le deep_link_return_url (https://moodle.example.com/mod/lti/contentitem_return.php?...), les types acceptés (ltiResourceLink), et accept_multiple.
  2. QuizLab affiche son catalogue : l’enseignant navigue, coche « Quiz : Les hooks React » et « Quiz : TypeScript avancé ».
  3. QuizLab renvoie un LtiDeepLinkingResponse — un JWT signé par l’outil, auto-posté vers le deep_link_return_url — contenant les content items : titre, url de lancement, éventuellement custom (paramètres par item), et lineItem (label, scoreMaximum, resourceId, tag) si l’item est notable.
  4. Retour automatique dans Moodle : le formulaire d’activité est pré-rempli — Activity name = « Quiz : Les hooks React », Tool URL = l’URL profonde de l’item, Custom parameters = les custom de l’item, et la section Grade calée sur le lineItem (note max = scoreMaximum).
  5. Si accept_multiple était vrai et que l’enseignant a choisi plusieurs items, Moodle crée plusieurs activités d’un coup, une par item, sans repasser par le formulaire.

C’est l’expérience à viser pour QuizLab : l’enseignant ne recopie jamais une URL, ne configure jamais une note maximale à la main.

💡 Pour un dev React : la vérification du JWT de la LtiDeepLinkingResponse est le premier moment où Moodle vérifie une signature de l’outil (et non l’inverse). C’est donc le premier endroit où un JWKS QuizLab inaccessible ou un kid absent de votre JWKS explose. Publiez votre JWKS avec un kid stable, mettez le même kid dans le header du JWT signé, et testez le flow Deep Linking en premier : s’il passe, votre chaîne de signature est bonne dans les deux sens.


6. Paramètres custom et variables de substitution

6.1 Syntaxe et niveaux

Les paramètres custom se saisissent une paire nom=valeur par ligne :

quizlab_theme=dark quiz_id=42 courseid=$Context.id fullname=$Person.name.full groups=$Moodle.Person.userGroupIds

Ils existent à deux niveaux, fusionnés au lancement :

  • niveau outil (champ Custom parameters de la configuration admin) : appliqués à tous les lancements de l’outil ;
  • niveau instance (champ Custom parameters de l’activité) : spécifiques à cette activité ; en cas de collision de nom, l’instance a le dernier mot dans la construction du claim.

Un troisième « niveau » existe de facto : les custom renvoyés par un content item de Deep Linking, qui atterrissent dans le champ instance.

6.2 Les variables de substitution

Toute valeur commençant par $ est traitée comme une variable de substitution (les capabilities de la spec LTI). Si Moodle la connaît et accepte de la fournir, il remplace ; sinon la valeur reste littéralement $Nom.De.Variable (signal utile en debug !). Les principales variables supportées par Moodle (définies dans lti_get_capabilities() de mod/lti/locallib.php) :

VariableValeur substituée
$Context.idid du contexte de cours tel qu’exposé par LTI (identifiant opaque du cours)
$Context.titlenom complet du cours
$Context.labelnom abrégé du cours
$Context.sourcedIdidnumber du cours (champ « Course ID number »)
$CourseSection.title / $CourseSection.labeléquivalents section de cours (mêmes valeurs cours dans Moodle)
$CourseSection.sourcedIdidnumber du cours
$ResourceLink.idid de l’instance d’activité (resource link)
$ResourceLink.titlenom de l’activité
$ResourceLink.descriptiondescription de l’activité
$User.idid numérique Moodle de l’utilisateur
$User.usernamelogin Moodle de l’utilisateur
$User.imageURL de l’avatar
$Person.name.fullnom complet
$Person.name.given / $Person.name.familyprénom / nom
$Person.email.primaryadresse de courriel
$Person.sourcedIdidnumber du profil utilisateur
$Membership.rolerôles LTI de l’utilisateur dans le contexte
$Result.sourcedId / $BasicOutcome.sourcedIdsourcedid de résultat (legacy Outcomes 1.1)
$Moodle.Person.userGroupIdsids des groupes Moodle de l’utilisateur dans le cours, séparés par des virgules (extension propre à Moodle)

La liste exacte évolue avec les versions ; en cas de doute, la source fait foi : cherchez lti_get_capabilities dans mod/lti/locallib.php de votre version.

⚠️ Piège : les substitutions liées à l’identité ($Person.name.full, $Person.email.primary, $User.username…) sont soumises aux réglages Privacy de l’outil. Si « Share launcher’s name with tool » vaut Never, $Person.name.full n’est pas substitué. Ne comptez jamais sur un custom param pour contourner la politique de confidentialité — c’est voulu.

6.3 Comment ils arrivent dans le JWT — et le piège des minuscules

Côté QuizLab, les paramètres arrivent dans le claim custom du JWT de lancement :

{ "https://purl.imsglobal.org/spec/lti/claim/custom": { "quizlab_theme": "dark", "quiz_id": "42", "courseid": "7", "fullname": "Alex Lemia", "groups": "12,15" } }

Trois règles à graver dans le marbre :

  1. Toutes les valeurs sont des chaînesquiz_id vaut "42", pas 42. Typage à faire côté outil (Zod est votre ami).
  2. Moodle passe les noms de paramètres en minuscules dans le claim custom : Quiz_ID=42 saisi par un enseignant arrive comme "quiz_id": "42". Héritage de LTI 1.1, où les paramètres custom devenaient des champs POST custom_quiz_id normalisés en minuscules. Piège archi-classique : l’outil documente camelCaseParam, l’enseignant le colle tel quel, et l’outil ne le retrouve pas.
  3. La substitution est faite au moment du launch, avec le contexte de l’utilisateur qui lance — deux étudiants du même cours reçoivent des $User.id différents, évidemment.

💡 Pour un dev React : blindez la lecture du claim custom côté QuizLab avec une normalisation systématique :

const CustomClaims = z .record(z.string(), z.string()) .transform((raw) => Object.fromEntries( Object.entries(raw).map(([k, v]) => [k.toLowerCase(), v]), ), ) .pipe( z.object(\{ quiz_id: z.coerce.number().optional(), quizlab_theme: z.enum(['dark', 'light']).default('light'), courseid: z.coerce.number().optional(), \}).passthrough(), ); const custom = CustomClaims.parse( idToken['https://purl.imsglobal.org/spec/lti/claim/custom'] ?? \{\}, );

Et documentez vos paramètres en minuscules dès le départ : vous éliminez le problème plutôt que de le gérer.


7. Réception des notes : AGS côté plateforme

Le service AGS (Assignment and Grade Services) est le canal par lequel QuizLab renvoie les résultats à Moodle. Le chapitre 10.2 a décrit le protocole ; regardons maintenant précisément ce que Moodle en fait.

7.1 Ce que le launch annonce

Si le service AGS est activé pour l’outil, le JWT de lancement contient le claim endpoint :

{ "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint": { "scope": [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems": "https://moodle.example.com/mod/lti/services.php/7/lineitems?type_id=3", "lineitem": "https://moodle.example.com/mod/lti/services.php/7/lineitems/31/lineitem?type_id=3" } }
  • lineitems : la collection des colonnes du contexte (le cours d’id de contexte 7) accessibles à cet outil ;
  • lineitem : présent seulement si l’instance lancée a déjà une colonne de note associée — la colonne « couplée » à l’activité.

Pour appeler ces URL, QuizLab obtient d’abord un access token sur https://moodle.example.com/mod/lti/token.php (grant client_credentials avec assertion private_key_jwt signée par la clé de QuizLab — que Moodle vérifie via le JWKS https://tool.example.com/api/lti/jwks).

7.2 Création de line items : des colonnes dans le carnet de notes

Quand QuizLab POST un line item sur la collection lineitems :

{ "label": "Quiz : Les hooks React", "scoreMaximum": 20, "resourceId": "quiz-42", "tag": "final", "resourceLinkId": "31" }

Moodle crée une colonne dans le carnet de notes du cours (un grade_item), enregistrée aussi dans la table mdl_ltiservice_gradebookservices pour mémoriser le lien avec l’outil, le resourceId et le tag. Deux cas :

  • line item couplé (resourceLinkId fourni et correspondant à une instance mod_lti) : la colonne est rattachée à l’activité ; c’est une colonne supplémentaire de la même activité ;
  • line item découplé (pas de resourceLinkId) : Moodle crée une colonne « orpheline » de type manuel dans le carnet de notes, regroupée dans une catégorie dédiée à l’outil. Elle vit indépendamment de toute activité — utile pour un outil qui gère plusieurs évaluations derrière un seul lien.

Par ailleurs, sans aucun appel AGS, dès qu’une instance External tool a une note configurée (section Grade) et que « Accept grades from the tool » est actif, Moodle crée la colonne de l’activité — c’est elle qu’annonce le claim lineitem du launch. Beaucoup d’outils simples ne créent jamais de line item : ils postent leurs scores sur celui que Moodle leur donne.

⚠️ Piège : si l’admin a choisi « Use this service for grade sync only », le POST de création de line item échouera en 403 (scope lineitem non accordé). QuizLab doit gérer ce cas : tester la présence du scope dans endpoint.scope avant de tenter la création, et se rabattre sur le lineitem couplé s’il existe.

7.3 Réception d’un score : le flow complet

Quand un étudiant termine un quiz, QuizLab poste un score :

POST https://moodle.example.com/mod/lti/services.php/7/lineitems/31/lineitem/scores?type_id=3 Authorization: Bearer <access_token> Content-Type: application/vnd.ims.lis.v1.score+json { "userId": "8f3d2a1c-...-sub-du-launch", "scoreGiven": 17.5, "scoreMaximum": 20, "activityProgress": "Completed", "gradingProgress": "FullyGraded", "timestamp": "2026-07-08T14:32:05+00:00", "comment": "17,5/20 — bien joué sur useEffect !" }

Voici ce qui se passe côté Moodle, de la requête HTTP à la note visible :

QuizLab Moodle (plateforme) | | | POST /mod/lti/token.php | | grant=client_credentials | | + client_assertion (JWT signé | | par la clé privée QuizLab) | |--------------------------------->| | |--> GET https://tool.example.com/api/lti/jwks | | vérifie la signature de l'assertion | | contrôle les scopes accordés au type d'outil | access_token (scopes AGS) | |<---------------------------------| | | | POST .../lineitem/scores | | Content-Type: ...score+json | |--------------------------------->| | | 1. valide le Bearer token + scope "score" | | 2. résout le line item -> grade_item | | (mdl_ltiservice_gradebookservices) | | 3. résout userId (sub) -> utilisateur Moodle | | et vérifie son inscription au cours | | 4. convertit le score : | | note = scoreGiven / scoreMaximum | | * grademax de la colonne | | (17.5/20 * 20 = 17.5) | | 5. selon gradingProgress : | | FullyGraded -> écrit la note finale | | PendingManual-> note à traiter manuellement | | (Pending/NotReady -> pas de note) | | 6. grade_update() -> carnet de notes | 204 No Content | (+ agrégations, historique de notes) |<---------------------------------| | | | L'étudiant voit 17,5/20 dans son carnet de notes

Points de détail qui comptent :

  • Conversion d’échelle : Moodle ne stocke pas scoreGiven tel quel ; il calcule la proportion scoreGiven / scoreMaximum et l’applique à la note maximale de la colonne (grademax). Si le quiz est sur 100 côté QuizLab et la colonne sur 20 côté Moodle, scoreGiven: 85, scoreMaximum: 100 donne 17/20. D’où l’importance d’envoyer toujours scoreMaximum avec scoreGiven.
  • userId : c’est le sub du launch (identifiant pseudonymisé, voir section 8), pas l’id numérique Moodle ni l’email.
  • Effacer une note : envoyer un score sans scoreGiven avec activityProgress: "Initialized" / gradingProgress: "NotReady" remet la note à néant.
  • comment est stocké comme feedback de la note, visible par l’étudiant dans son rapport de notes.

7.4 activityProgress et gradingProgress : effets concrets

gradingProgressEffet côté Moodle
FullyGradedLa note est écrite comme note finale : visible dans le carnet de notes (selon les réglages d’affichage du gradebook), incluse dans les agrégations. Le cas nominal.
PendingManualLa note nécessite une intervention : elle n’est pas publiée comme note finale tant qu’un enseignant n’intervient pas (marquage « à corriger »). Utile pour les questions ouvertes corrigées à la main dans l’outil.
PendingNotation en cours côté outil : pas de note finale écrite.
NotReady / FailedPas de note ; Failed signale un échec du processus de notation côté outil.

activityProgress (Initialized, Started, InProgress, Submitted, Completed) documente l’avancement de l’activité ; Moodle s’en sert surtout comme métadonnée (et pour l’effacement, cf. supra) — c’est gradingProgress qui pilote l’écriture de la note.

7.5 « Accept grades from the tool » et l’interaction avec les scopes

Deux verrous se superposent, et il faut les deux :

  1. Le service AGS de l’outil (réglage admin, section 2.3) : détermine les scopes que token.php accepte de délivrer. « Do not use this service » = aucun token AGS, point final.
  2. « Accept grades from the tool » (privacy, niveau outil ou délégué à l’instance) : détermine si l’instance a une colonne de note couplée et si le claim lineitem est exposé au launch.

Matrice des situations courantes :

Service AGSAccept gradesRésultat
grade sync + column mgmtAlways/cochéCas nominal : colonne couplée + création de colonnes libres
grade sync onlycochéScores sur la colonne couplée uniquement
grade sync + column mgmtdécoché (instance)Pas de colonne couplée ; l’outil peut quand même créer des line items découplés (les scopes sont là) — source classique de confusion pour les enseignants
Do not use this servicepeu importeAucun retour de note possible (hormis Basic Outcomes si outil 1.1)

⚠️ Piège : le token AGS et ses scopes sont accordés par type d’outil, pas par instance. Décocher « Accept grades » sur une activité n’empêche pas techniquement l’outil de créer un line item découplé dans le même cours. Si la politique de l’établissement est « pas de notes de cet outil », c’est le réglage service AGS de l’outil qu’il faut fermer, pas la case de l’instance.

💡 Pour un dev React : les access tokens de token.php ont une durée de vie courte (3600 s) et leur obtention coûte un aller-retour + une vérification JWKS. Mettez-les en cache côté QuizLab par couple (plateforme, scopes) — Redis ou mémoire avec TTL — et implémentez un retry sur 401 avec invalidation du cache. Autre bonne pratique : postez les scores dans une file (queue) plutôt qu’en synchrone dans la requête de l’étudiant ; si Moodle est lent ou indisponible, l’étudiant n’a pas à le subir, et vous rejouez les échecs.

📚 Aller plus loin : LTI Assignment and Grade Services 2.0  pour la sémantique complète, et le code Moodle mod/lti/service/gradebookservices/ pour l’implémentation (les classes lineitem, scores, results du service).


8. Confidentialité et RGPD

Un lancement LTI est un transfert de données personnelles vers un tiers. En Europe, cela engage la responsabilité de l’établissement (responsable de traitement) vis-à-vis de l’éditeur de l’outil (sous-traitant, le plus souvent). Moodle fournit les mécanismes ; à l’admin de les utiliser en cohérence avec le registre de traitements et les DPA signés.

8.1 Ce qui part vers l’outil, réglage par réglage

Toujours transmis, quels que soient les réglages :

  • le sub : identifiant utilisateur pseudonymisé, stable pour un même utilisateur sur une même plateforme/déploiement, mais qui ne révèle ni le nom, ni l’email, ni l’id Moodle en tant que tel ;
  • les rôles LTI dans le contexte (roles) ;
  • le contexte de cours (id opaque, titre, label — le titre du cours peut lui-même être signifiant !) ;
  • la locale, des informations sur la plateforme, le resource link.

Transmis seulement si autorisé :

RéglageClaims concernés
Share launcher’s name with toolname, given_name, family_name
Share launcher’s email with toolemail
(photo de profil via $User.image en custom param)URL d’avatar
NRPS activél’annuaire des inscrits, filtré selon les deux réglages ci-dessus

Avec « Delegate to teacher », la décision descend au niveau de chaque activité — souple, mais difficile à auditer à l’échelle d’un site : préférez Always/Never décidé par l’admin après revue du contrat de l’outil.

8.2 Consentement et information

Quand le nom ou l’email est partagé, Moodle affiche à l’utilisateur, au premier lancement d’un outil, un écran d’information indiquant les données transmises (comportement lié à l’orchestration du launch et aux réglages de confidentialité du site). Cela relève de l’information (art. 13 RGPD) plus que du consentement : la base légale d’un outil pédagogique imposé est rarement le consentement (mission d’intérêt public ou intérêt légitime, selon le contexte), mais l’utilisateur doit savoir ce qui part et vers qui. Complétez avec :

  • la déclaration du sous-traitant dans le registre des traitements ;
  • l’API Privacy de Moodle : mod_lti implémente les privacy providers, donc les exports/suppressions RGPD (Site administration > Users > Privacy and policies) couvrent les données LTI stockées côté Moodle — mais pas les données stockées côté outil, qui relèvent du DPA avec l’éditeur.

8.3 Ce que voit l’enseignant

L’enseignant qui configure une activité voit les cases de confidentialité déléguées, le bouton Select content, et les notes qui reviennent — mais pas le contenu des JWT ni les appels de service. En cas de doute d’un enseignant sur « ce que l’outil sait », la réponse se trouve dans la configuration admin de l’outil (View configuration details + réglages Privacy), pas dans l’activité.

💡 Pour un dev React : côté QuizLab, concevez le modèle de données pour fonctionner sans nom ni email : la clé primaire d’un utilisateur est (iss, client_id, sub) — jamais l’email. Traitez name et email comme des enrichissements optionnels et affichez un fallback (« Participant 8f3d ») quand ils manquent. Vous serez compatible avec les plateformes les plus restrictives, et votre DPA sera plus simple à négocier.

📚 Aller plus loin : le programme 1EdTech « TrustEd Apps » évalue les pratiques de confidentialité des outils LTI ; côté Moodle, la doc « Privacy API » sur moodledev.io explique les providers que mod_lti implémente.


9. Débogage côté Moodle

Vous brancherez QuizLab, et ça ne marchera pas du premier coup. Personne n’y arrive du premier coup. Cette section est le guide de survie : les erreurs classiques, leur diagnostic, où regarder dans Moodle, et comment isoler le problème.

9.1 Activer le debugging Moodle

Avant tout : Site administration > Development > Debugging (Administration du site > Développement > Débogage) :

  • Debug messages : DEVELOPER: extra Moodle debug messages for developers ;
  • Display debug messages (Afficher les messages de débogage) : coché — les erreurs LTI, souvent avalées en production, deviennent visibles ;

ou dans config.php d’un Moodle de dev :

@error_reporting(E_ALL | E_STRICT); @ini_set('display_errors', '1'); $CFG->debug = (E_ALL | E_STRICT); $CFG->debugdisplay = 1;

Consultez aussi les logs : Site administration > Reports > Logs pour l’activité (lancements, ajouts d’activité), et les logs du serveur web/PHP pour les stack traces. Les échecs sur token.php et services.php (appels serveur-à-serveur de l’outil) n’apparaissent que dans les logs serveur — l’admin ne les voit nulle part dans l’UI.

9.2 La table de diagnostic des erreurs classiques

SymptômeCause probableDiagnostic / remède
« Invalid nonce » au launchRejeu du même nonce, ou horloges désynchronisées entre le serveur Moodle et l’outil (clock skew) : les claims iat/exp du JWT sortent de la fenêtre de toléranceVérifiez que les deux serveurs sont sur NTP (timedatectl status / w32tm /query /status). Un décalage de 2-3 minutes suffit. Côté outil, augmentez la clockTolerance de la vérification JWT (30-60 s), jamais plus. Vérifiez aussi que l’outil ne re-consomme pas un launch (bouton Back, double submit)
« Invalid deployment id » / deployment mismatchL’outil valide le deployment_id du JWT contre sa config, et elles divergent (outil ré-enregistré côté Moodle → nouvel id ; ou faute de frappe)Comparez le claim deployment_id du JWT reçu avec la valeur dans View configuration details. Rappel : le deployment_id Moodle = id de la ligne mdl_lti_types, il change si vous supprimez/recréez l’outil
« Unknown client_id » / token refusé sur token.phpL’outil s’authentifie avec un client_id qui ne correspond à aucun type d’outil actifRequête SQL 9.3 n° 1 ; vérifiez que l’outil n’est pas resté « Pending »
Échec de vérification de la réponse Deep Linking, ou 401 sur token.php alors que le client_id est bonMoodle n’arrive pas à joindre le JWKS de l’outil : certificat auto-signé, DNS interne, firewall sortant, ou JWKS sans le bon kidDepuis le serveur Moodle : curl -v https://tool.example.com/api/lti/jwks. Si TLS invalide → vrai certificat (Let’s Encrypt) ou tunnel ; si inaccessible → règle firewall sortante ; si accessible → vérifiez que le kid du header JWT existe dans le JWKS
Erreur au retour de auth.php : redirect_uri inconnuLe redirect_uri envoyé par l’outil n’est pas dans la liste Redirection URI(s)Comparaison exacte (slash final, http vs https, port). Ajoutez l’URI manquante côté Moodle
Boucle infinie login → launch → login, ou outil « déconnecté » dans l’iframeCookies tiers bloqués dans l’iframe (voir 9.4)Passez le Launch container en « New window » : si ça marche, c’est bien un problème de cookies en contexte tiers
Page blanche dans l’iframeL’outil envoie X-Frame-Options: DENY ou une CSP frame-ancestors sans le domaine MoodleConsole navigateur (erreur explicite). Côté Next.js : ajustez les headers pour autoriser frame-ancestors https://moodle.example.com
« This tool is pending activation »Dynamic Registration effectué mais outil jamais activéManage tools → Activate
Les notes ne remontent pas, aucune erreur visibleScope AGS manquant, userId (sub) inconnu, utilisateur désinscrit, ou colonne suppriméeLogs serveur Moodle sur services.php ; requêtes SQL 9.3 n° 3 et 4 ; vérifiez le code HTTP reçu par l’outil (403 = scope, 404 = line item, 200/204 = accepté)

⚠️ Piège : le clock skew est vicieux parce qu’il est intermittent : une VM dont l’horloge dérive lentement fonctionne le lundi et casse le jeudi. Symptômes typiques : « Invalid nonce », « token expired », ou des launchs qui échouent seulement pour certains utilisateurs (ceux qui tombent sur le serveur web désynchronisé derrière le load balancer). Réflexe : NTP partout, monitoring du décalage, avant de toucher au code.

9.3 Regarder dans la base : les tables LTI

Les tables utiles (préfixe mdl_ par défaut) :

TableContenu
mdl_lti_typesLes outils enregistrés (nom, baseurl, état, clientid) — l’id est le deployment_id
mdl_lti_types_configLes réglages détaillés de chaque outil en paires nom/valeur (keyset URL, initiate login, redirection URIs, services, privacy…)
mdl_ltiLes instances d’activité External tool (une par activité dans un cours)
mdl_lti_tool_settingsLes paires clé/valeur du service Tool Settings
mdl_ltiservice_gradebookservicesLe lien entre line items AGS et grade items du carnet de notes
mdl_lti_access_tokensLes access tokens délivrés par token.php (hash, scopes, validité)

Requêtes de diagnostic prêtes à l’emploi :

-- 1. Lister les outils LTI 1.3 : client_id, deployment_id, état -- state : 1 = actif (configured), 2 = pending, 3 = rejeté SELECT t.id AS deployment_id, t.name, t.baseurl, t.clientid, t.state, t.course, t.ltiversion FROM mdl_lti_types t WHERE t.ltiversion = '1.3.0' ORDER BY t.id; -- 2. Dump complet de la configuration d'un outil (remplacer 3 par l'id) SELECT c.name, c.value FROM mdl_lti_types_config c WHERE c.typeid = 3 ORDER BY c.name; -- Repérez : publickeyset, initiatelogin, redirectionuris, ltiservice_gradesynchronization, -- ltiservice_memberships, sendname, sendemailaddr, acceptgrades, customparameters -- 3. Les instances d'un outil et leur cours SELECT l.id AS resource_link_id, l.course, l.name, l.typeid, l.toolurl, l.instructorcustomparameters, l.grade FROM mdl_lti l WHERE l.typeid = 3; -- 4. Les line items AGS et leur colonne de carnet de notes SELECT gbs.id, gbs.gradeitemid, gbs.courseid, gbs.toolproxyid, gbs.typeid, gbs.ltilinkid, gbs.resourceid, gbs.tag, gi.itemname, gi.grademax FROM mdl_ltiservice_gradebookservices gbs JOIN mdl_grade_items gi ON gi.id = gbs.gradeitemid WHERE gbs.typeid = 3; -- 5. Les derniers access tokens délivrés à l'outil (scopes accordés, validité) SELECT at.id, at.typeid, at.scope, at.validuntil, FROM_UNIXTIME(at.validuntil) AS expire_le FROM mdl_lti_access_tokens at WHERE at.typeid = 3 ORDER BY at.id DESC LIMIT 5;

La requête n° 5 est précieuse : elle vous dit exactement quels scopes Moodle a mis dans le dernier token de QuizLab — la vérité terrain quand vous soupçonnez un problème de service AGS/NRPS mal configuré.

⚠️ Piège : les noms de clés dans mdl_lti_types_config ne correspondent pas mot à mot aux libellés de l’UI (acceptgrades vaut 0/1/2 pour Never/Always/Delegate — ordre non intuitif, vérifiez les constantes LTI_SETTING_* dans mod/lti/locallib.php avant d’interpréter). Ne modifiez jamais ces tables à la main sur un site de production : passez par l’UI ou les fonctions de locallib.php, sinon les caches (MUC) serviront d’anciennes valeurs.

9.4 Le cas d’école : cookies tiers dans l’iframe

Le bug le plus fréquent en 2026, et le plus déroutant, mérite sa propre sous-section. Scénario : le launch fonctionne, QuizLab affiche sa page… puis toute action est « déconnectée », ou le flow OIDC boucle entre /api/lti/login et auth.php.

Cause : en Launch container « Embed », QuizLab tourne dans une iframe sur le domaine moodle.example.com. Ses cookies sont donc des cookies tiers :

  • ils doivent être émis avec SameSite=None; Secure pour être seulement éligibles en contexte tiers — un SameSite=Lax (défaut de la plupart des frameworks, y compris les cookies de session par défaut) est silencieusement ignoré ;
  • Safari (ITP) bloque les cookies tiers même avec SameSite=None ; Chrome les restreint selon la configuration utilisateur/entreprise ;
  • le flow OIDC lui-même dépend d’un cookie : l’outil pose un cookie state/nonce à l’étape login et le relit à l’étape launch. Cookie bloqué → state introuvable → l’outil renvoie vers le login → boucle.

Solutions, de la plus simple à la plus robuste :

  1. Launch container « New window » : plus d’iframe, plus de contexte tiers. Le remède immédiat, au prix de l’expérience intégrée. À utiliser au minimum comme test de diagnostic.
  2. Cookies Partitioned (CHIPS) : Set-Cookie: session=...; SameSite=None; Secure; Partitioned — le cookie est partitionné par site de premier niveau ; supporté par Chrome/Edge et adopté progressivement ailleurs. Bonne solution d’avenir, pas encore universelle.
  3. Se passer de cookies pour la corrélation OIDC : transporter le state autrement (paramètre signé, cache serveur indexé par state), et après le launch, une session portée par l’URL ou un token en postMessage. C’est la stratégie des outils LTI sérieux.
  4. Le storage postMessage LTI (spec 1EdTech « LTI Client Side postMessages ») : Moodle expose un frame de stockage que l’outil peut utiliser via postMessage pour stocker le state à la place des cookies. Support à vérifier selon la version, mais c’est la réponse standardisée au problème.

💡 Pour un dev React : dans QuizLab/Next.js, la combinaison qui marche partout : (1) à l’étape login, stocker {state, nonce} dans un cache serveur (Redis, TTL 5 min) indexé par state, plutôt que dans un cookie ; (2) à l’étape launch, valider l’id_token, créer la session applicative et la matérialiser en JWT propre à QuizLab passé dans l’URL de redirection interne (ou en cookie Partitioned avec fallback) ; (3) détecter les cookies bloqués côté client (document.hasStorageAccess?.(), tentative d’écriture/lecture) et afficher un bouton « Ouvrir dans une nouvelle fenêtre » plutôt qu’un écran cassé.

9.5 Isoler le problème avec un outil/une plateforme de référence

Quand « Moodle + QuizLab » échoue, la question est : lequel des deux est en tort ? Réflexe : remplacer un des deux par une implémentation de référence.

  • saLTIre (https://saltire.lti.app) : simulateur mature qui joue au choix la plateforme ou l’outil. Enregistrez saLTIre-outil dans votre Moodle : si le launch saLTIre fonctionne, votre Moodle est bien configuré et le problème est chez QuizLab. Inversement, lancez QuizLab depuis saLTIre-plateforme pour tester l’outil sans Moodle. saLTIre affiche l’intégralité des messages (JWT décodé, claims, appels de service) — inestimable.
  • 1EdTech Reference Implementation (https://lti-ri.imsglobal.org) : la plateforme/outil de référence officielle d’1EdTech, même logique, utilisée pour la certification.

Méthode en trois temps : (1) Moodle → saLTIre-outil : valide la config plateforme ; (2) saLTIre-plateforme → QuizLab : valide l’outil ; (3) seulement ensuite, Moodle → QuizLab, en comparant les JWT observés en (1) et (2).

Dernier outil de la panoplie : l’onglet Réseau du navigateur avec « Preserve log » activé, pour capturer la chaîne complète login → auth.php → launch (des redirections et auto-submit de formulaires que la navigation efface sinon), et jwt.io  pour décoder les id_token capturés — en environnement de dev uniquement, un JWT de prod contient des données personnelles.

📚 Aller plus loin : le « LTI Bootcamp » d’1EdTech et la page moodledev.io « LTI concepts » listent d’autres outils de test ; le forum moodle.org « External tool (LTI) » est l’endroit où chercher un message d’erreur exact — la plupart des erreurs de cette section y ont des fils dédiés.


Ce qu’il faut retenir

  • mod_lti est l’implémentation plateforme de Moodle : activité « External tool », sélecteur d’activités, page « LTI External tools » des cours, et l’écran admin central Site administration > Plugins > Activity modules > External tool > Manage tools.
  • Les endpoints plateforme de Moodle sont fixes et dérivés du wwwroot : auth.php (autorisation OIDC), token.php (tokens de service), certs.php (JWKS), openid-configuration.php (dynamic registration). L’issuer est l’URL racine du site.
  • Deux voies d’enregistrement : manuelle (tous les champs passés en revue en section 2 — retenez surtout Redirection URIs exactes, Keyset URL vs RSA key, Tool configuration usage, et les trois options du service AGS) et Dynamic Registration (une URL à coller, un flow automatisé, un état Pending à activer). Après enregistrement, « View configuration details » donne le client_id, le deployment_id et les endpoints à reporter côté outil.
  • Trois portées : outil de site (la norme), outil de cours (expérimentation enseignant), configuration par instance (legacy). Le deployment_id de Moodle est l’id de mdl_lti_types.
  • Les custom parameters (nom=valeur, un par ligne, niveaux outil et instance) supportent les variables $Context.id, $ResourceLink.id, $Person.name.full, $Moodle.Person.userGroupIds… et arrivent dans le claim custom en minuscules, en chaînes, filtrés par les réglages de confidentialité.
  • AGS côté Moodle : les line items deviennent des colonnes du carnet de notes (couplées à l’activité ou découplées), les scores sont convertis proportionnellement (scoreGiven/scoreMaximum × note max de la colonne), et gradingProgress pilote la publication (FullyGraded = note visible, PendingManual = intervention requise). Deux verrous : le service AGS de l’outil (scopes) et « Accept grades from the tool » (colonne couplée).
  • Confidentialité : seul le sub pseudonymisé part toujours ; nom et email dépendent des réglages Privacy (Never / Delegate to teacher / Always). Concevez vos outils pour fonctionner sans.
  • Débogage : NTP avant tout (nonce/clock skew), Redirection URIs exactes, JWKS de l’outil joignable depuis le serveur Moodle, cookies tiers en iframe (SameSite=None; Secure, ITP, CHIPS — test au « New window »), debugging DEVELOPER, tables mdl_lti_types / mdl_lti_types_config / mdl_lti / mdl_ltiservice_gradebookservices / mdl_lti_access_tokens, et isolation avec saLTIre ou lti-ri.imsglobal.org.

Nous avons vu Moodle consommer des outils. Dans le prochain chapitre, on inverse complètement la perspective : 04-moodle-outil.md explore Moodle en tant que fournisseur LTI — la fonctionnalité « Publish as LTI tool » (LTI Provider), qui permet de publier un cours ou une activité Moodle pour qu’une autre plateforme (un autre Moodle, Canvas, ou même votre app Next.js jouant cette fois le rôle de plateforme) les lance à distance, avec provisioning des utilisateurs et synchronisation des notes dans l’autre sens. Vous connaissez maintenant les deux moitiés du protocole ; il ne reste qu’à retourner la carte.