ajoute Moodle LTI 1.3 (V2), une 6e façon de rejoindre une classe

LTI authentifie l'élève et le fait rejoindre sa classe (même code que
le code/QR existant), pas lancer un lab précis - relecture du
CLAUDE.md qui liste LTI avec les autres façons d'ajouter des élèves.
packbackbooks/lti-1p3-tool (Apache-2.0, recommandé par le CLAUDE.md),
trois ponts vers Redis/cookies/MariaDB, élève retrouvé par identité
Moodle stable pour ne jamais créer de doublon. Enregistrement d'une
plateforme/d'un déploiement par commande Artisan (opération rare,
faite à la main par Adam), jamais une page d'administration.

Corrige au passage un vrai bug préexistant trouvé en vérifiant en réel
sur le serveur : withoutMiddleware(ValidateCsrfToken::class) sur une
route ne retire jamais le vrai middleware CSRF (ValidateCsrfToken est
un alias déprécié de PreventRequestForgery, pas la classe réellement
enregistrée) - /csp-rapport avait le même défaut depuis l'étape 14,
invisible faute de surveillance. Corrigé pour les deux par la bonne
méthode (validateCsrfTokens except, dans bootstrap/app.php).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-14 13:51:47 +02:00
parent 777dd1cbc8
commit 91d7cde4c1
27 changed files with 1293 additions and 9 deletions

View file

@ -0,0 +1,76 @@
# ADR 0010 — LTI 1.3 Moodle : une 6ᵉ façon de rejoindre une classe, pas un lancement de lab
- **Date** : 2026-10-14
- **Statut** : accepté
## Contexte
CLAUDE.md, section 6 : « Moodle LTI 1.3 (V2) » est listé **avec** les autres
façons d'ajouter des élèves (code/QR, import CSV, saisie rapide, invitation
e-mail) — pas dans la description de l'espace de lab. Avancé à la demande
explicite d'Adam (bloc C terminé, V2 lancée par anticipation).
## Décisions
**LTI sert à rejoindre une classe, pas à lancer un lab précis.** Un
lancement Moodle authentifie l'élève (OIDC + jeton signé) et le fait
atterrir sur son tableau de bord habituel (`eleve.show`) — depuis là, il
choisit un lab ou un devoir normalement, par le portail. Construire un
lien direct vers un lab précis aurait demandé de router l'admission dans
une séance (`SeanceController::admettre`), bien plus complexe, pour un
besoin que le CLAUDE.md ne demande pas.
**L'URL de lancement réutilise `code_classe` existant**
(`/lti/lancement/{code_classe}`), pas un nouveau jeton ou une nouvelle
table de liaison. Exactement la même donnée que celle déjà affichée pour
« Faire rejoindre tes élèves » (code/QR) — LTI est juste une 6ᵉ porte vers
la même classe, pas un concept à part.
**`packbackbooks/lti-1p3-tool` (Apache-2.0)**, recommandé par le CLAUDE.md.
Bibliothèque bas niveau (pas de dépendance à un framework) : il faut lui
fournir trois implémentations (`ICache`, `ICookie`, `IDatabase`, dans
`App\Services\Lti\`) qui s'appuient sur Redis, les cookies et les tables
`lti_plateformes`/`lti_deploiements` déjà en place.
**Élève provisionné automatiquement au premier lancement, retrouvé par
`(classe_id, lti_emetteur, lti_sub)` ensuite** — jamais de doublon de
compte à chaque connexion. Prénom/nom lus depuis les claims standards
(`given_name`/`family_name`, puis `name`, jamais inventés : « Élève » /
« Moodle » si Moodle n'envoie rien). Code secret généré au hasard, haché,
jamais affiché : cet élève se connecte toujours via Moodle, jamais par
identifiant/code comme les autres.
**Cookie `SameSite=None; Secure` pour le jeton d'état OIDC.** Sans ça, le
cookie ne survit jamais au POST final que Moodle envoie depuis son propre
domaine — chaque lancement échouerait silencieusement (« Please make sure
you have cookies… », message déjà prévu par la bibliothèque pour ce cas
précis, qui n'arrive qu'avec un réglage de cookie insuffisant).
**Sécurité x-pack… non, pas de rapport ici — mais même logique que pour
Elasticsearch/Ceph** : une seule clé RSA par plateforme Moodle enregistrée
(générée par le portail lui-même à l'enregistrement, `phpseclib4\Crypt\RSA`,
jamais collée par Adam), exposée en public via `/lti/cles.json` — c'est le
fonctionnement normal du protocole (Moodle doit pouvoir vérifier les jetons
que *nous* lui envoyons, si jamais le portail appelle un service LTI plus
tard), pas une fuite.
**Enregistrement manuel, par commande Artisan, jamais une page
d'administration.** Une plateforme Moodle se configure une fois par
établissement, par Adam, avec des informations lues dans l'administration
Moodle — une page web pour ça serait une fonctionnalité sans usage réel
(CLAUDE.md, section 8). `lti:enregistrer-plateforme` et
`lti:enregistrer-deploiement`, voir `docs/runbooks/lti-moodle.md`.
## Conséquences
- Testé avec un jeton LTI réellement signé (RS256, paire de clés générée
dans le test), jeu de clés de « Moodle » simulé : lancement validé de
bout en bout, élève créé avec le bon prénom/nom, deuxième lancement du
même élève ne crée pas de deuxième compte. Pas de vrai Moodle disponible
pour vérifier en conditions réelles — à refaire dès qu'Adam a un Moodle
de test.
- En dette : pas de web service LTI (AGS pour les notes, NRPS pour la
liste des inscrits, Deep Linking) — seul le lancement de ressource est
géré, comme décidé ci-dessus. Les classes de la bibliothèque pour ces
services existent déjà (`LtiAssignmentsGradesService` etc.) si le besoin
devient réel.

View file

@ -41,3 +41,4 @@ retirer la ligne (et le noter dans le journal) quand c'est fait.
| Ceph : bootstrap fait à la main, pas encore rejouable par Ansible | Comme Kolla à l'étape 2 : acceptable pour une démo, à automatiser si ce cluster doit durer. Le fichier systemd du loop device est suivi dans `infra/ceph/` mais déployé à la main. | Si la démo devient durable | 2026-10-14 |
| WebDAV de l'élève : stockage local, pas S3 ; pas de quota par élève | Dossier local (`storage/app/webdav/`), partage le disque serré avec OpenStack et le portail — pas de limite de taille par élève. rclone sait servir un backend S3 sans changement d'architecture (voir [ADR 0008](adr/0008-webdav-eleve-auth-request.md)) : à basculer en même temps que le bucket S3 des devoirs (`docs/runbooks/stockage-s3.md`), et à ajouter un quota si l'usage réel le demande. | Quand le bucket S3 existe / si l'usage le demande | 2026-10-14 |
| Recherche (Elasticsearch) : pas de pagination ni de facettes | Catalogue minuscule aujourd'hui (quelques labs, un seul vrai `cours.md`) : inutile tant que ça reste le cas. Voir [ADR 0009](adr/0009-elasticsearch-interne-sans-securite.md). | Si le catalogue grossit réellement | 2026-10-14 |
| LTI Moodle : pas de services AGS (notes)/NRPS (liste des inscrits)/Deep Linking, pas vérifié avec un vrai Moodle | Seul le lancement de ressource est géré (rejoindre une classe) — décision assumée, voir [ADR 0010](adr/0010-lti-moodle-comme-sixieme-facon-de-rejoindre.md). Testé avec un jeton réellement signé mais un Moodle simulé ; à revérifier dès qu'un vrai Moodle de test existe. | Si le besoin de notes/liste d'inscrits devient réel ; dès qu'un Moodle de test existe | 2026-10-14 |

View file

@ -1274,3 +1274,27 @@ Le réflexe acquis avec Ceph et WebDAV (vérifier l'état réel avant d'agir, me
**Suite**
Bloc C restant : WebDAV et Ceph faits, Elasticsearch fait. ElasticSearch avait été écarté du MCP développeurs par Adam (pas pour l'instant). Prochains chantiers confirmés par Adam : LTI Moodle 1.3, puis SSO ENT (Keycloak) — normalement des étapes V2/V3 du produit, avancées à la demande explicite d'Adam.
## 2026-10-14 — LTI Moodle 1.3, avancé en V2 (sur demande d'Adam)
Bloc C terminé. Adam a demandé d'avancer directement sur la V2 : LTI Moodle 1.3, puis SSO ENT. Détail des choix : [ADR 0010](adr/0010-lti-moodle-comme-sixieme-facon-de-rejoindre.md), marche à suivre : `docs/runbooks/lti-moodle.md`.
**Fait**
- Relu le CLAUDE.md avant de commencer : « Moodle LTI 1.3 » est listé avec les autres façons d'ajouter des élèves (code/QR, CSV…), pas avec l'espace de lab — a recadré toute la conception : LTI fait simplement rejoindre une classe (comme le code/QR existant), pas lancer un lab précis.
- `packbackbooks/lti-1p3-tool` (Apache-2.0, recommandé par le CLAUDE.md) : trois ponts vers Redis/cookies/MariaDB (`App\Services\Lti\{CacheLti,CookieLti,DatabaseLti}`), deux nouvelles tables (`lti_plateformes`, `lti_deploiements`), deux colonnes sur `eleves` (`lti_emetteur`, `lti_sub`, pour retrouver le même élève à chaque lancement sans créer de doublon).
- URL de lancement par classe : `https://syllabuslab.fr/lti/lancement/<code_classe>` — réutilise le code déjà affiché pour « Faire rejoindre tes élèves », pas un nouveau concept. Visible sur la page de la classe, section « Lier à Moodle ».
- Élève provisionné automatiquement au premier lancement (prénom/nom lus dans les claims Moodle, jamais inventés ; « Élève »/« Moodle » si Moodle n'en envoie pas), retrouvé ensuite par identité Moodle stable — jamais de compte en double.
- Deux commandes Artisan (`lti:enregistrer-plateforme`, `lti:enregistrer-deploiement`) : l'enregistrement d'un Moodle est une opération rare, faite à la main par Adam avec des informations lues dans l'administration Moodle — jamais une page web pour ça (CLAUDE.md : pas de fonctionnalité sans besoin réel).
- Cookie d'état OIDC en `SameSite=None; Secure` : sans ça, le cookie ne survit jamais au POST final que Moodle envoie depuis son propre domaine — repéré en lisant le code de la bibliothèque (son message d'erreur dédié à ce cas précis), pas après un échec en vrai.
- 7 tests Pest, dont un lancement **réellement signé** de bout en bout (vraie paire de clés RSA générée dans le test, jeu de clés de « Moodle » simulé par un faux connecteur de service) : élève créé avec le bon prénom/nom, un deuxième lancement du même élève ne crée pas de deuxième compte, classe inconnue/archivée affiche une erreur propre au lieu d'une 500.
**Comprendre**
Pas de vrai Moodle pour vérifier ce chantier en conditions réelles (contrairement à Ceph/WebDAV/Elasticsearch, tous vérifiés sur le vrai serveur) — la bibliothèque LTI elle-même construit ses propres tests avec un jeton signé et un jeu de clés simulé plutôt qu'un vrai IMS Validator, et j'ai repris exactement ce principe : le test ne fait confiance à rien de magique, il signe un vrai jeton avec une vraie clé et vérifie que le portail le valide et en tire les bonnes conséquences. C'est la meilleure vérification possible sans Moodle réel, mais ça reste noté en dette — un vrai lancement Moodle peut révéler des détails qu'aucune bibliothèque de test ne reproduit (comportement des cookies tiers d'un vrai navigateur, en particulier).
**Bug préexistant trouvé en vérifiant en réel sur le serveur** : `/lti/lancement` répondait 419 (jeton CSRF) malgré un `->withoutMiddleware(ValidateCsrfToken::class)` posé sur la route — jamais vu en local, les tests Pest ne passent jamais par le vrai middleware CSRF. Cause : `ValidateCsrfToken` est une classe dépréciée, simple alias de `PreventRequestForgery` (la vraie classe réellement enregistrée dans le groupe `web`) — `withoutMiddleware` compare des noms de classe exacts, et ne retirait donc jamais la bonne. `/csp-rapport` avait exactement le même défaut depuis l'étape 14, invisible faute de surveillance de ses échecs (un navigateur qui échoue à poster un rapport CSP ne prévient personne). Corrigé pour les deux à la fois, par la vraie méthode (`$middleware->validateCsrfTokens(except: [...])` dans `bootstrap/app.php`), vérifié en réel après coup : les trois routes (`csp-rapport`, `lti/connexion`, `lti/lancement`) répondent maintenant comme attendu (204/400/200) au lieu de 419.
**Suite**
Prochain, confirmé par Adam : SSO ENT (Keycloak), V3 du CLAUDE.md.

View file

@ -0,0 +1,74 @@
# Runbook — Moodle LTI 1.3
6ᵉ façon de rejoindre une classe (V2, CLAUDE.md section 6) : un élève
clique sur l'activité Moodle, atterrit connecté sur son tableau de bord
Syllabus habituel. Détail des choix :
[ADR 0010](../adr/0010-lti-moodle-comme-sixieme-facon-de-rejoindre.md).
## Étape 1 — Côté Moodle : ajouter Syllabus comme outil externe
Administration du site → Plugins → Outils externes → Gérer les outils →
Configurer un outil manuellement. Renseigner :
| Champ Moodle | Valeur |
|---|---|
| URL de l'outil | `https://syllabuslab.fr/lti/lancement/<code de la classe>` (visible sur la page de la classe, section « Lier à Moodle ») |
| LTI version | 1.3 |
| Initiate login URL | `https://syllabuslab.fr/lti/connexion` |
| Redirection URIs | la même URL de lancement que ci-dessus |
| Public key type | Clé publique au format JWKS |
| Jeu de clés publiques (URL) | `https://syllabuslab.fr/lti/cles.json` |
Après enregistrement, Moodle **affiche** un « Platform ID », un
« Client ID », une « Public keyset URL », une « Access token URL » et une
« Authentication request URL » — ce sont ces valeurs-là (celles de
Moodle, pas les nôtres) qu'il faut passer à l'étape 2.
## Étape 2 — Côté serveur : enregistrer la plateforme
```
ssh paillasse
cd /var/www/portail
sudo -u www-data HOME=/tmp php8.3 artisan lti:enregistrer-plateforme \
"<Platform ID affiché par Moodle>" \
"<Client ID affiché par Moodle>" \
"<Public keyset URL affichée par Moodle>" \
"<Access token URL affichée par Moodle>" \
"<Authentication request URL affichée par Moodle>"
```
La commande génère et stocke une paire de clés RSA pour le portail (jamais
collée par Adam) et rappelle les deux adresses déjà saisies à l'étape 1.
## Étape 3 — Ajouter l'outil à un cours, puis enregistrer le déploiement
Dans le cours Moodle : ajouter une activité « Outil externe » pointant
vers Syllabus. Une fois ajoutée, Moodle affiche un **Deployment ID** pour
cette combinaison outil/cours — à enregistrer :
```
sudo -u www-data HOME=/tmp php8.3 artisan lti:enregistrer-deploiement <id_plateforme> <deployment_id>
```
`<id_plateforme>` est l'identifiant numérique affiché à l'étape 2
(« Plateforme #… enregistrée »).
## Vérifier que ça fonctionne
Un élève qui clique sur l'activité dans Moodle doit atterrir connecté sur
`/eleve`, dans la bonne classe. En cas d'échec, Moodle affiche en général
le message d'erreur renvoyé par le portail (page « Le lancement depuis
Moodle n'a pas fonctionné ») :
| Message | Cause probable |
|---|---|
| Classe introuvable ou archivée | Le code dans l'URL de lancement ne correspond à aucune classe active |
| LTI 1.3 Registration not found… | Étape 2 pas faite, ou Platform ID/Client ID mal recopiés |
| Unable to find deployment | Étape 3 pas faite pour ce cours précis |
| Please make sure you have cookies… | Cookies tiers bloqués côté navigateur de l'élève, ou horloge du serveur décalée |
## Un élève change d'identité Moodle ou rejoint depuis deux cours
Peu probable, mais si ça arrive : un nouveau compte serait créé (l'élève
est retrouvé par `lti_sub`, propre à Moodle). Pas de fusion automatique —
à faire à la main si ça se produit réellement.